]> git.openstreetmap.org Git - nominatim.git/commitdiff
make reverse API work with sqlite
authorSarah Hoffmann <lonvia@denofr.de>
Fri, 13 Oct 2023 09:03:13 +0000 (11:03 +0200)
committerSarah Hoffmann <lonvia@denofr.de>
Mon, 23 Oct 2023 15:19:12 +0000 (17:19 +0200)
nominatim/api/reverse.py
nominatim/db/sqlalchemy_functions.py
nominatim/db/sqlalchemy_schema.py
nominatim/db/sqlalchemy_types.py
test/python/api/test_api_reverse.py

index d46733f0d950fe9eb8a051f3e21e6b916dec14de..e0c7e460d2940b362c79987507c6e859a88c9685 100644 (file)
@@ -19,7 +19,6 @@ import nominatim.api.results as nres
 from nominatim.api.logging import log
 from nominatim.api.types import AnyPoint, DataLayer, ReverseDetails, GeometryFormat, Bbox
 from nominatim.db.sqlalchemy_types import Geometry
-import nominatim.db.sqlalchemy_functions as snfn
 
 # In SQLAlchemy expression which compare with NULL need to be expressed with
 # the equal sign.
@@ -88,7 +87,7 @@ def _locate_interpolation(table: SaFromClause) -> SaLabel:
 def _is_address_point(table: SaFromClause) -> SaColumn:
     return sa.and_(table.c.rank_address == 30,
                    sa.or_(table.c.housenumber != None,
-                          table.c.name.has_key('addr:housename')))
+                          sa.func.JsonHasKey(table.c.name, 'addr:housename')))
 
 
 def _get_closest(*rows: Optional[SaRow]) -> Optional[SaRow]:
@@ -371,7 +370,7 @@ class ReverseGeocoder:
             inner = sa.select(t, sa.literal(0.0).label('distance'))\
                       .where(t.c.rank_search.between(5, MAX_RANK_PARAM))\
                       .where(t.c.geometry.intersects(WKT_PARAM))\
-                      .where(snfn.select_index_placex_geometry_reverse_lookuppolygon('placex'))\
+                      .where(sa.func.PlacexGeometryReverseLookuppolygon())\
                       .order_by(sa.desc(t.c.rank_search))\
                       .limit(50)\
                       .subquery('area')
@@ -401,10 +400,7 @@ class ReverseGeocoder:
                       .where(t.c.rank_search > address_rank)\
                       .where(t.c.rank_search <= MAX_RANK_PARAM)\
                       .where(t.c.indexed_status == 0)\
-                      .where(snfn.select_index_placex_geometry_reverse_lookupplacenode('placex'))\
-                      .where(t.c.geometry
-                                .ST_Buffer(sa.func.reverse_place_diameter(t.c.rank_search))
-                                .intersects(WKT_PARAM))\
+                      .where(sa.func.IntersectsReverseDistance(t, WKT_PARAM))\
                       .order_by(sa.desc(t.c.rank_search))\
                       .limit(50)\
                       .subquery('places')
@@ -413,7 +409,7 @@ class ReverseGeocoder:
                 return _select_from_placex(inner, False)\
                     .join(touter, touter.c.geometry.ST_Contains(inner.c.geometry))\
                     .where(touter.c.place_id == address_id)\
-                    .where(inner.c.distance < sa.func.reverse_place_diameter(inner.c.rank_search))\
+                    .where(sa.func.IsBelowReverseDistance(inner.c.distance, inner.c.rank_search))\
                     .order_by(sa.desc(inner.c.rank_search), inner.c.distance)\
                     .limit(1)
 
@@ -440,10 +436,9 @@ class ReverseGeocoder:
                   .where(t.c.indexed_status == 0)\
                   .where(t.c.linked_place_id == None)\
                   .where(self._filter_by_layer(t))\
-                  .where(t.c.geometry
-                                .ST_Buffer(sa.func.reverse_place_diameter(t.c.rank_search))
-                                .intersects(WKT_PARAM))\
+                  .where(t.c.geometry.intersects(sa.func.ST_Expand(WKT_PARAM, 0.001)))\
                   .order_by(sa.desc(t.c.rank_search))\
+                  .order_by('distance')\
                   .limit(50)\
                   .subquery()
 
@@ -514,16 +509,13 @@ class ReverseGeocoder:
                       .where(t.c.rank_search <= MAX_RANK_PARAM)\
                       .where(t.c.indexed_status == 0)\
                       .where(t.c.country_code.in_(ccodes))\
-                      .where(snfn.select_index_placex_geometry_reverse_lookupplacenode('placex'))\
-                      .where(t.c.geometry
-                                .ST_Buffer(sa.func.reverse_place_diameter(t.c.rank_search))
-                                .intersects(WKT_PARAM))\
+                      .where(sa.func.IntersectsReverseDistance(t, WKT_PARAM))\
                       .order_by(sa.desc(t.c.rank_search))\
                       .limit(50)\
                       .subquery('area')
 
                 return _select_from_placex(inner, False)\
-                    .where(inner.c.distance < sa.func.reverse_place_diameter(inner.c.rank_search))\
+                    .where(sa.func.IsBelowReverseDistance(inner.c.distance, inner.c.rank_search))\
                     .order_by(sa.desc(inner.c.rank_search), inner.c.distance)\
                     .limit(1)
 
index 6eed880367dfd9947b9e87ef17c646ab869f6955..6a5809bdd3de3df069ef58314b22adc52f04117a 100644 (file)
@@ -15,19 +15,110 @@ from sqlalchemy.ext.compiler import compiles
 
 from nominatim.typing import SaColumn
 
-# pylint: disable=abstract-method,missing-function-docstring,consider-using-f-string
+# pylint: disable=all
 
-def select_index_placex_geometry_reverse_lookuppolygon(table: str) -> 'sa.TextClause':
-    """ Create an expression with the necessary conditions over a placex
-        table that the index 'idx_placex_geometry_reverse_lookupPolygon'
-        can be used.
+class PlacexGeometryReverseLookuppolygon(sa.sql.functions.GenericFunction[bool]):
+    """ Check for conditions that allow partial index use on
+        'idx_placex_geometry_reverse_lookupPolygon'.
+
+        Needs to be constant, so that the query planner picks them up correctly
+        in prepared statements.
     """
-    return sa.text(f"ST_GeometryType({table}.geometry) in ('ST_Polygon', 'ST_MultiPolygon')"
-                   f" AND {table}.rank_address between 4 and 25"
-                   f" AND {table}.type != 'postcode'"
-                   f" AND {table}.name is not null"
-                   f" AND {table}.indexed_status = 0"
-                   f" AND {table}.linked_place_id is null")
+    type = sa.Boolean()
+    name = 'PlacexGeometryReverseLookuppolygon'
+    inherit_cache = True
+
+
+@compiles(PlacexGeometryReverseLookuppolygon) # type: ignore[no-untyped-call, misc]
+def _default_intersects(element: SaColumn,
+                        compiler: 'sa.Compiled', **kw: Any) -> str:
+    return ("(ST_GeometryType(placex.geometry) in ('ST_Polygon', 'ST_MultiPolygon')"
+            " AND placex.rank_address between 4 and 25"
+            " AND placex.type != 'postcode'"
+            " AND placex.name is not null"
+            " AND placex.indexed_status = 0"
+            " AND placex.linked_place_id is null)")
+
+
+@compiles(PlacexGeometryReverseLookuppolygon, 'sqlite') # type: ignore[no-untyped-call, misc]
+def _sqlite_intersects(element: SaColumn,
+                       compiler: 'sa.Compiled', **kw: Any) -> str:
+    return ("(ST_GeometryType(placex.geometry) in ('POLYGON', 'MULTIPOLYGON')"
+            " AND placex.rank_address between 4 and 25"
+            " AND placex.type != 'postcode'"
+            " AND placex.name is not null"
+            " AND placex.indexed_status = 0"
+            " AND placex.linked_place_id is null)")
+
+
+class IntersectsReverseDistance(sa.sql.functions.GenericFunction[bool]):
+    type = sa.Boolean()
+    name = 'IntersectsReverseDistance'
+    inherit_cache = True
+
+    def __init__(self, table: sa.Table, geom: SaColumn) -> None:
+        super().__init__(table.c.geometry, # type: ignore[no-untyped-call]
+                         table.c.rank_search, geom)
+        self.tablename = table.name
+
+
+@compiles(IntersectsReverseDistance) # type: ignore[no-untyped-call, misc]
+def default_reverse_place_diameter(element: SaColumn,
+                                   compiler: 'sa.Compiled', **kw: Any) -> str:
+    table = element.tablename
+    return f"{table}.rank_address between 4 and 25"\
+           f" AND {table}.type != 'postcode'"\
+           f" AND {table}.name is not null"\
+           f" AND {table}.linked_place_id is null"\
+           f" AND {table}.osm_type = 'N'" + \
+           " AND ST_Buffer(%s, reverse_place_diameter(%s)) && %s" % \
+               tuple(map(lambda c: compiler.process(c, **kw), element.clauses))
+
+
+@compiles(IntersectsReverseDistance, 'sqlite') # type: ignore[no-untyped-call, misc]
+def sqlite_reverse_place_diameter(element: SaColumn,
+                                  compiler: 'sa.Compiled', **kw: Any) -> str:
+    geom1, rank, geom2 = list(element.clauses)
+    table = element.tablename
+
+    return (f"{table}.rank_address between 4 and 25"\
+            f" AND {table}.type != 'postcode'"\
+            f" AND {table}.name is not null"\
+            f" AND {table}.linked_place_id is null"\
+            f" AND {table}.osm_type = 'N'"\
+             " AND MbrIntersects(%s, ST_Expand(%s, 14.0 * exp(-0.2 * %s) - 0.03))"\
+            f" AND {table}.place_id IN"\
+             " (SELECT place_id FROM placex_place_node_areas"\
+             "  WHERE ROWID IN (SELECT ROWID FROM SpatialIndex"\
+             "  WHERE f_table_name = 'placex_place_node_areas'"\
+             "  AND search_frame = %s))") % (
+                compiler.process(geom1, **kw),
+                compiler.process(geom2, **kw),
+                compiler.process(rank, **kw),
+                compiler.process(geom2, **kw))
+
+
+class IsBelowReverseDistance(sa.sql.functions.GenericFunction[bool]):
+    type = sa.Boolean()
+    name = 'IsBelowReverseDistance'
+    inherit_cache = True
+
+
+@compiles(IsBelowReverseDistance) # type: ignore[no-untyped-call, misc]
+def default_is_below_reverse_distance(element: SaColumn,
+                                      compiler: 'sa.Compiled', **kw: Any) -> str:
+    dist, rank = list(element.clauses)
+    return "%s < reverse_place_diameter(%s)" % (compiler.process(dist, **kw),
+                                                compiler.process(rank, **kw))
+
+
+@compiles(IsBelowReverseDistance, 'sqlite') # type: ignore[no-untyped-call, misc]
+def sqlite_is_below_reverse_distance(element: SaColumn,
+                                     compiler: 'sa.Compiled', **kw: Any) -> str:
+    dist, rank = list(element.clauses)
+    return "%s < 14.0 * exp(-0.2 * %s) - 0.03" % (compiler.process(dist, **kw),
+                                                  compiler.process(rank, **kw))
+
 
 def select_index_placex_geometry_reverse_lookupplacenode(table: str) -> 'sa.TextClause':
     """ Create an expression with the necessary conditions over a placex
@@ -84,6 +175,22 @@ def sqlite_json_array_each(element: SaColumn, compiler: 'sa.Compiled', **kw: Any
     return "json_each(%s)" % compiler.process(element.clauses, **kw)
 
 
+class JsonHasKey(sa.sql.functions.GenericFunction[bool]):
+    """ Return elements of a json array as a set.
+    """
+    type = sa.Boolean()
+    name = 'JsonHasKey'
+    inherit_cache = True
+
+
+@compiles(JsonHasKey) # type: ignore[no-untyped-call, misc]
+def compile_json_has_key(element: SaColumn,
+                         compiler: 'sa.Compiled', **kw: Any) -> str:
+    arg1, arg2 = list(element.clauses)
+    return "%s->%s is not null" % (compiler.process(arg1, **kw),
+                                   compiler.process(arg2, **kw))
+
+
 class Greatest(sa.sql.functions.GenericFunction[Any]):
     """ Function to compute maximum of all its input parameters.
     """
index 3b769eb3e34fff7dd16ee12e3d2946c5887392f9..7dd1e0ce0b046182b6224eab7b5ec16769719b96 100644 (file)
@@ -13,6 +13,7 @@ import sqlalchemy as sa
 from sqlalchemy.dialects.postgresql import HSTORE, ARRAY, JSONB, array
 from sqlalchemy.dialects.sqlite import JSON as sqlite_json
 
+import nominatim.db.sqlalchemy_functions #pylint: disable=unused-import
 from nominatim.db.sqlalchemy_types import Geometry
 
 class PostgresTypes:
index 4171d70b0d19ce20d764cea22a3ec55a7e87d5c9..df2bf1504e11ef6b87bebc96a679d0c20294e2ef 100644 (file)
@@ -41,6 +41,71 @@ def _spatialite_distance_spheroid(element: SaColumn,
     return "Distance(%s, true)" % compiler.process(element.clauses, **kw)
 
 
+class Geometry_IsLineLike(sa.sql.expression.FunctionElement[bool]):
+    """ Check if the geometry is a line or multiline.
+    """
+    type = sa.Boolean()
+    name = 'Geometry_IsLineLike'
+    inherit_cache = True
+
+
+@compiles(Geometry_IsLineLike) # type: ignore[no-untyped-call, misc]
+def _default_is_line_like(element: SaColumn,
+                          compiler: 'sa.Compiled', **kw: Any) -> str:
+    return "ST_GeometryType(%s) IN ('ST_LineString', 'ST_MultiLineString')" % \
+               compiler.process(element.clauses, **kw)
+
+
+@compiles(Geometry_IsLineLike, 'sqlite') # type: ignore[no-untyped-call, misc]
+def _sqlite_is_line_like(element: SaColumn,
+                         compiler: 'sa.Compiled', **kw: Any) -> str:
+    return "ST_GeometryType(%s) IN ('LINESTRING', 'MULTILINESTRING')" % \
+               compiler.process(element.clauses, **kw)
+
+
+class Geometry_IsAreaLike(sa.sql.expression.FunctionElement[bool]):
+    """ Check if the geometry is a polygon or multipolygon.
+    """
+    type = sa.Boolean()
+    name = 'Geometry_IsLineLike'
+    inherit_cache = True
+
+
+@compiles(Geometry_IsAreaLike) # type: ignore[no-untyped-call, misc]
+def _default_is_area_like(element: SaColumn,
+                          compiler: 'sa.Compiled', **kw: Any) -> str:
+    return "ST_GeometryType(%s) IN ('ST_Polygon', 'ST_MultiPolygon')" % \
+               compiler.process(element.clauses, **kw)
+
+
+@compiles(Geometry_IsAreaLike, 'sqlite') # type: ignore[no-untyped-call, misc]
+def _sqlite_is_area_like(element: SaColumn,
+                         compiler: 'sa.Compiled', **kw: Any) -> str:
+    return "ST_GeometryType(%s) IN ('POLYGON', 'MULTIPOLYGON')" % \
+               compiler.process(element.clauses, **kw)
+
+
+class Geometry_IntersectsBbox(sa.sql.expression.FunctionElement[bool]):
+    """ Check if the bounding boxes of the given geometries intersect.
+    """
+    type = sa.Boolean()
+    name = 'Geometry_IntersectsBbox'
+    inherit_cache = True
+
+
+@compiles(Geometry_IntersectsBbox) # type: ignore[no-untyped-call, misc]
+def _default_intersects(element: SaColumn,
+                        compiler: 'sa.Compiled', **kw: Any) -> str:
+    arg1, arg2 = list(element.clauses)
+    return "%s && %s" % (compiler.process(arg1, **kw), compiler.process(arg2, **kw))
+
+
+@compiles(Geometry_IntersectsBbox, 'sqlite') # type: ignore[no-untyped-call, misc]
+def _sqlite_intersects(element: SaColumn,
+                       compiler: 'sa.Compiled', **kw: Any) -> str:
+    return "MbrIntersects(%s)" % compiler.process(element.clauses, **kw)
+
+
 class Geometry(types.UserDefinedType): # type: ignore[type-arg]
     """ Simplified type decorator for PostGIS geometry. This type
         only supports geometries in 4326 projection.
@@ -82,15 +147,15 @@ class Geometry(types.UserDefinedType): # type: ignore[type-arg]
     class comparator_factory(types.UserDefinedType.Comparator): # type: ignore[type-arg]
 
         def intersects(self, other: SaColumn) -> 'sa.Operators':
-            return self.op('&&')(other)
+            return Geometry_IntersectsBbox(self, other)
+
 
         def is_line_like(self) -> SaColumn:
-            return sa.func.ST_GeometryType(self, type_=sa.String).in_(('ST_LineString',
-                                                                       'ST_MultiLineString'))
+            return Geometry_IsLineLike(self)
+
 
         def is_area(self) -> SaColumn:
-            return sa.func.ST_GeometryType(self, type_=sa.String).in_(('ST_Polygon',
-                                                                       'ST_MultiPolygon'))
+            return Geometry_IsAreaLike(self)
 
 
         def ST_DWithin(self, other: SaColumn, distance: SaColumn) -> SaColumn:
@@ -119,7 +184,8 @@ class Geometry(types.UserDefinedType): # type: ignore[type-arg]
 
 
         def ST_ClosestPoint(self, other: SaColumn) -> SaColumn:
-            return sa.func.ST_ClosestPoint(self, other, type_=Geometry)
+            return sa.func.coalesce(sa.func.ST_ClosestPoint(self, other, type_=Geometry),
+                                    other)
 
 
         def ST_Buffer(self, other: SaColumn) -> SaColumn:
@@ -161,11 +227,13 @@ SQLITE_FUNCTION_ALIAS = (
     ('ST_AsGeoJSON', sa.Text, 'AsGeoJSON'),
     ('ST_AsKML', sa.Text, 'AsKML'),
     ('ST_AsSVG', sa.Text, 'AsSVG'),
+    ('ST_LineLocatePoint', sa.Float, 'ST_Line_Locate_Point'),
+    ('ST_LineInterpolatePoint', sa.Float, 'ST_Line_Interpolate_Point'),
 )
 
 def _add_function_alias(func: str, ftype: type, alias: str) -> None:
     _FuncDef = type(func, (sa.sql.functions.GenericFunction, ), {
-        "type": ftype,
+        "type": ftype(),
         "name": func,
         "identifier": func,
         "inherit_cache": True})
@@ -181,4 +249,17 @@ for alias in SQLITE_FUNCTION_ALIAS:
     _add_function_alias(*alias)
 
 
+class ST_DWithin(sa.sql.functions.GenericFunction[bool]):
+    type = sa.Boolean()
+    name = 'ST_DWithin'
+    inherit_cache = True
+
 
+@compiles(ST_DWithin, 'sqlite') # type: ignore[no-untyped-call, misc]
+def default_json_array_each(element: SaColumn, compiler: 'sa.Compiled', **kw: Any) -> str:
+    geom1, geom2, dist = list(element.clauses)
+    return "(MbrIntersects(%s, ST_Expand(%s, %s)) = 1 AND ST_Distance(%s, %s) <= %s)" % (
+        compiler.process(geom1, **kw), compiler.process(geom2, **kw),
+        compiler.process(dist, **kw),
+        compiler.process(geom1, **kw), compiler.process(geom2, **kw),
+        compiler.process(dist, **kw))
index d9ab2cb87efbc945059339935838cd3924d716c5..414115e113783a575c6a8d52453e1e04f1a80a98 100644 (file)
@@ -16,20 +16,23 @@ import pytest
 
 import nominatim.api as napi
 
-def test_reverse_rank_30(apiobj):
+API_OPTIONS = {'reverse'}
+
+def test_reverse_rank_30(apiobj, frontend):
     apiobj.add_placex(place_id=223, class_='place', type='house',
                       housenumber='1',
                       centroid=(1.3, 0.7),
                       geometry='POINT(1.3 0.7)')
 
-    result = apiobj.api.reverse((1.3, 0.7))
+    api = frontend(apiobj, options=API_OPTIONS)
+    result = api.reverse((1.3, 0.7))
 
     assert result is not None
     assert result.place_id == 223
 
 
 @pytest.mark.parametrize('country', ['de', 'us'])
-def test_reverse_street(apiobj, country):
+def test_reverse_street(apiobj, frontend, country):
     apiobj.add_placex(place_id=990, class_='highway', type='service',
                       rank_search=27, rank_address=27,
                       name = {'name': 'My Street'},
@@ -37,17 +40,19 @@ def test_reverse_street(apiobj, country):
                       country_code=country,
                       geometry='LINESTRING(9.995 10, 10.005 10)')
 
-    assert apiobj.api.reverse((9.995, 10)).place_id == 990
+    api = frontend(apiobj, options=API_OPTIONS)
+    assert api.reverse((9.995, 10)).place_id == 990
 
 
-def test_reverse_ignore_unindexed(apiobj):
+def test_reverse_ignore_unindexed(apiobj, frontend):
     apiobj.add_placex(place_id=223, class_='place', type='house',
                       housenumber='1',
                       indexed_status=2,
                       centroid=(1.3, 0.7),
                       geometry='POINT(1.3 0.7)')
 
-    result = apiobj.api.reverse((1.3, 0.7))
+    api = frontend(apiobj, options=API_OPTIONS)
+    result = api.reverse((1.3, 0.7))
 
     assert result is None
 
@@ -62,7 +67,7 @@ def test_reverse_ignore_unindexed(apiobj):
                                               (0.70003, napi.DataLayer.MANMADE | napi.DataLayer.RAILWAY, 225),
                                               (0.70003, napi.DataLayer.MANMADE | napi.DataLayer.NATURAL, 225),
                                               (5, napi.DataLayer.ADDRESS, 229)])
-def test_reverse_rank_30_layers(apiobj, y, layer, place_id):
+def test_reverse_rank_30_layers(apiobj, frontend, y, layer, place_id):
     apiobj.add_placex(place_id=223, class_='place', type='house',
                       housenumber='1',
                       rank_address=30,
@@ -90,21 +95,23 @@ def test_reverse_rank_30_layers(apiobj, y, layer, place_id):
                       rank_search=30,
                       centroid=(1.3, 5))
 
-    assert apiobj.api.reverse((1.3, y), layers=layer).place_id == place_id
+    api = frontend(apiobj, options=API_OPTIONS)
+    assert api.reverse((1.3, y), layers=layer).place_id == place_id
 
 
-def test_reverse_poi_layer_with_no_pois(apiobj):
+def test_reverse_poi_layer_with_no_pois(apiobj, frontend):
     apiobj.add_placex(place_id=223, class_='place', type='house',
                       housenumber='1',
                       rank_address=30,
                       rank_search=30,
                       centroid=(1.3, 0.70001))
 
-    assert apiobj.api.reverse((1.3, 0.70001), max_rank=29,
+    api = frontend(apiobj, options=API_OPTIONS)
+    assert api.reverse((1.3, 0.70001), max_rank=29,
                               layers=napi.DataLayer.POI) is None
 
 
-def test_reverse_housenumber_on_street(apiobj):
+def test_reverse_housenumber_on_street(apiobj, frontend):
     apiobj.add_placex(place_id=990, class_='highway', type='service',
                       rank_search=27, rank_address=27,
                       name = {'name': 'My Street'},
@@ -116,12 +123,13 @@ def test_reverse_housenumber_on_street(apiobj):
                       housenumber='23',
                       centroid=(10.0, 10.00001))
 
-    assert apiobj.api.reverse((10.0, 10.0), max_rank=30).place_id == 991
-    assert apiobj.api.reverse((10.0, 10.0), max_rank=27).place_id == 990
-    assert apiobj.api.reverse((10.0, 10.00001), max_rank=30).place_id == 991
+    api = frontend(apiobj, options=API_OPTIONS)
+    assert api.reverse((10.0, 10.0), max_rank=30).place_id == 991
+    assert api.reverse((10.0, 10.0), max_rank=27).place_id == 990
+    assert api.reverse((10.0, 10.00001), max_rank=30).place_id == 991
 
 
-def test_reverse_housenumber_interpolation(apiobj):
+def test_reverse_housenumber_interpolation(apiobj, frontend):
     apiobj.add_placex(place_id=990, class_='highway', type='service',
                       rank_search=27, rank_address=27,
                       name = {'name': 'My Street'},
@@ -138,10 +146,11 @@ def test_reverse_housenumber_interpolation(apiobj):
                        centroid=(10.0, 10.00001),
                        geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
 
-    assert apiobj.api.reverse((10.0, 10.0)).place_id == 992
+    api = frontend(apiobj, options=API_OPTIONS)
+    assert api.reverse((10.0, 10.0)).place_id == 992
 
 
-def test_reverse_housenumber_point_interpolation(apiobj):
+def test_reverse_housenumber_point_interpolation(apiobj, frontend):
     apiobj.add_placex(place_id=990, class_='highway', type='service',
                       rank_search=27, rank_address=27,
                       name = {'name': 'My Street'},
@@ -153,12 +162,13 @@ def test_reverse_housenumber_point_interpolation(apiobj):
                        centroid=(10.0, 10.00001),
                        geometry='POINT(10.0 10.00001)')
 
-    res = apiobj.api.reverse((10.0, 10.0))
+    api = frontend(apiobj, options=API_OPTIONS)
+    res = api.reverse((10.0, 10.0))
     assert res.place_id == 992
     assert res.housenumber == '42'
 
 
-def test_reverse_tiger_number(apiobj):
+def test_reverse_tiger_number(apiobj, frontend):
     apiobj.add_placex(place_id=990, class_='highway', type='service',
                       rank_search=27, rank_address=27,
                       name = {'name': 'My Street'},
@@ -171,11 +181,12 @@ def test_reverse_tiger_number(apiobj):
                      centroid=(10.0, 10.00001),
                      geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
 
-    assert apiobj.api.reverse((10.0, 10.0)).place_id == 992
-    assert apiobj.api.reverse((10.0, 10.00001)).place_id == 992
+    api = frontend(apiobj, options=API_OPTIONS)
+    assert api.reverse((10.0, 10.0)).place_id == 992
+    assert api.reverse((10.0, 10.00001)).place_id == 992
 
 
-def test_reverse_point_tiger(apiobj):
+def test_reverse_point_tiger(apiobj, frontend):
     apiobj.add_placex(place_id=990, class_='highway', type='service',
                       rank_search=27, rank_address=27,
                       name = {'name': 'My Street'},
@@ -188,12 +199,13 @@ def test_reverse_point_tiger(apiobj):
                      centroid=(10.0, 10.00001),
                      geometry='POINT(10.0 10.00001)')
 
-    res = apiobj.api.reverse((10.0, 10.0))
+    api = frontend(apiobj, options=API_OPTIONS)
+    res = api.reverse((10.0, 10.0))
     assert res.place_id == 992
     assert res.housenumber == '1'
 
 
-def test_reverse_low_zoom_address(apiobj):
+def test_reverse_low_zoom_address(apiobj, frontend):
     apiobj.add_placex(place_id=1001, class_='place', type='house',
                       housenumber='1',
                       rank_address=30,
@@ -207,11 +219,12 @@ def test_reverse_low_zoom_address(apiobj):
                       geometry="""POLYGON((59.3 80.70001, 59.3001 80.70001,
                                         59.3001 80.70101, 59.3 80.70101, 59.3 80.70001))""")
 
-    assert apiobj.api.reverse((59.30005, 80.7005)).place_id == 1001
-    assert apiobj.api.reverse((59.30005, 80.7005), max_rank=18).place_id == 1002
+    api = frontend(apiobj, options=API_OPTIONS)
+    assert api.reverse((59.30005, 80.7005)).place_id == 1001
+    assert api.reverse((59.30005, 80.7005), max_rank=18).place_id == 1002
 
 
-def test_reverse_place_node_in_area(apiobj):
+def test_reverse_place_node_in_area(apiobj, frontend):
     apiobj.add_placex(place_id=1002, class_='place', type='town',
                       name={'name': 'Town Area'},
                       rank_address=16,
@@ -226,7 +239,8 @@ def test_reverse_place_node_in_area(apiobj):
                       rank_search=18,
                       centroid=(59.30004, 80.70055))
 
-    assert apiobj.api.reverse((59.30004, 80.70055)).place_id == 1003
+    api = frontend(apiobj, options=API_OPTIONS)
+    assert api.reverse((59.30004, 80.70055)).place_id == 1003
 
 
 @pytest.mark.parametrize('layer,place_id', [(napi.DataLayer.MANMADE, 225),
@@ -234,7 +248,7 @@ def test_reverse_place_node_in_area(apiobj):
                                             (napi.DataLayer.NATURAL, 227),
                                             (napi.DataLayer.MANMADE | napi.DataLayer.RAILWAY, 225),
                                             (napi.DataLayer.MANMADE | napi.DataLayer.NATURAL, 225)])
-def test_reverse_larger_area_layers(apiobj, layer, place_id):
+def test_reverse_larger_area_layers(apiobj, frontend, layer, place_id):
     apiobj.add_placex(place_id=225, class_='man_made', type='dam',
                       name={'name': 'Dam'},
                       rank_address=0,
@@ -251,17 +265,19 @@ def test_reverse_larger_area_layers(apiobj, layer, place_id):
                       rank_search=16,
                       centroid=(1.3, 0.70005))
 
-    assert apiobj.api.reverse((1.3, 0.7), layers=layer).place_id == place_id
+    api = frontend(apiobj, options=API_OPTIONS)
+    assert api.reverse((1.3, 0.7), layers=layer).place_id == place_id
 
 
-def test_reverse_country_lookup_no_objects(apiobj):
+def test_reverse_country_lookup_no_objects(apiobj, frontend):
     apiobj.add_country('xx', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))')
 
-    assert apiobj.api.reverse((0.5, 0.5)) is None
+    api = frontend(apiobj, options=API_OPTIONS)
+    assert api.reverse((0.5, 0.5)) is None
 
 
 @pytest.mark.parametrize('rank', [4, 30])
-def test_reverse_country_lookup_country_only(apiobj, rank):
+def test_reverse_country_lookup_country_only(apiobj, frontend, rank):
     apiobj.add_country('xx', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))')
     apiobj.add_placex(place_id=225, class_='place', type='country',
                       name={'name': 'My Country'},
@@ -270,10 +286,11 @@ def test_reverse_country_lookup_country_only(apiobj, rank):
                       country_code='xx',
                       centroid=(0.7, 0.7))
 
-    assert apiobj.api.reverse((0.5, 0.5), max_rank=rank).place_id == 225
+    api = frontend(apiobj, options=API_OPTIONS)
+    assert api.reverse((0.5, 0.5), max_rank=rank).place_id == 225
 
 
-def test_reverse_country_lookup_place_node_inside(apiobj):
+def test_reverse_country_lookup_place_node_inside(apiobj, frontend):
     apiobj.add_country('xx', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))')
     apiobj.add_placex(place_id=225, class_='place', type='state',
                       osm_type='N',
@@ -283,11 +300,12 @@ def test_reverse_country_lookup_place_node_inside(apiobj):
                       country_code='xx',
                       centroid=(0.5, 0.505))
 
-    assert apiobj.api.reverse((0.5, 0.5)).place_id == 225
+    api = frontend(apiobj, options=API_OPTIONS)
+    assert api.reverse((0.5, 0.5)).place_id == 225
 
 
 @pytest.mark.parametrize('gtype', list(napi.GeometryFormat))
-def test_reverse_geometry_output_placex(apiobj, gtype):
+def test_reverse_geometry_output_placex(apiobj, frontend, gtype):
     apiobj.add_country('xx', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))')
     apiobj.add_placex(place_id=1001, class_='place', type='house',
                       housenumber='1',
@@ -302,34 +320,37 @@ def test_reverse_geometry_output_placex(apiobj, gtype):
                       country_code='xx',
                       centroid=(0.5, 0.5))
 
-    assert apiobj.api.reverse((59.3, 80.70001), geometry_output=gtype).place_id == 1001
-    assert apiobj.api.reverse((0.5, 0.5), geometry_output=gtype).place_id == 1003
+    api = frontend(apiobj, options=API_OPTIONS)
+    assert api.reverse((59.3, 80.70001), geometry_output=gtype).place_id == 1001
+    assert api.reverse((0.5, 0.5), geometry_output=gtype).place_id == 1003
 
 
-def test_reverse_simplified_geometry(apiobj):
+def test_reverse_simplified_geometry(apiobj, frontend):
     apiobj.add_placex(place_id=1001, class_='place', type='house',
                       housenumber='1',
                       rank_address=30,
                       rank_search=30,
                       centroid=(59.3, 80.70001))
 
+    api = frontend(apiobj, options=API_OPTIONS)
     details = dict(geometry_output=napi.GeometryFormat.GEOJSON,
                    geometry_simplification=0.1)
-    assert apiobj.api.reverse((59.3, 80.70001), **details).place_id == 1001
+    assert api.reverse((59.3, 80.70001), **details).place_id == 1001
 
 
-def test_reverse_interpolation_geometry(apiobj):
+def test_reverse_interpolation_geometry(apiobj, frontend):
     apiobj.add_osmline(place_id=992,
                        parent_place_id=990,
                        startnumber=1, endnumber=3, step=1,
                        centroid=(10.0, 10.00001),
                        geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
 
-    assert apiobj.api.reverse((10.0, 10.0), geometry_output=napi.GeometryFormat.TEXT)\
+    api = frontend(apiobj, options=API_OPTIONS)
+    assert api.reverse((10.0, 10.0), geometry_output=napi.GeometryFormat.TEXT)\
                      .geometry['text'] == 'POINT(10 10.00001)'
 
 
-def test_reverse_tiger_geometry(apiobj):
+def test_reverse_tiger_geometry(apiobj, frontend):
     apiobj.add_placex(place_id=990, class_='highway', type='service',
                       rank_search=27, rank_address=27,
                       name = {'name': 'My Street'},
@@ -342,7 +363,8 @@ def test_reverse_tiger_geometry(apiobj):
                      centroid=(10.0, 10.00001),
                      geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
 
-    output = apiobj.api.reverse((10.0, 10.0),
+    api = frontend(apiobj, options=API_OPTIONS)
+    output = api.reverse((10.0, 10.0),
                                 geometry_output=napi.GeometryFormat.GEOJSON).geometry['geojson']
 
     assert json.loads(output) == {'coordinates': [10, 10.00001], 'type': 'Point'}