]> git.openstreetmap.org Git - nominatim.git/commitdiff
make details API work with sqlite incl. unit tests
authorSarah Hoffmann <lonvia@denofr.de>
Thu, 12 Oct 2023 13:31:20 +0000 (15:31 +0200)
committerSarah Hoffmann <lonvia@denofr.de>
Mon, 23 Oct 2023 15:19:12 +0000 (17:19 +0200)
nominatim/api/lookup.py
nominatim/api/results.py
nominatim/db/sqlalchemy_functions.py
nominatim/db/sqlalchemy_types.py
test/python/api/test_api_details.py

index e9181f473784aec219c91f08acc3708e7dd3e516..b1f05c638104aaea72ae8eb4969505b44278521d 100644 (file)
@@ -77,8 +77,8 @@ async def find_in_osmline(conn: SearchConnection, place: ntyp.PlaceRef,
         sql = sql.where(t.c.osm_id == place.osm_id).limit(1)
         if place.osm_class and place.osm_class.isdigit():
             sql = sql.order_by(sa.func.greatest(0,
         sql = sql.where(t.c.osm_id == place.osm_id).limit(1)
         if place.osm_class and place.osm_class.isdigit():
             sql = sql.order_by(sa.func.greatest(0,
-                                    sa.func.least(int(place.osm_class) - t.c.endnumber),
-                                           t.c.startnumber - int(place.osm_class)))
+                                                int(place.osm_class) - t.c.endnumber,
+                                                t.c.startnumber - int(place.osm_class)))
     else:
         return None
 
     else:
         return None
 
@@ -163,11 +163,10 @@ async def get_detailed_place(conn: SearchConnection, place: ntyp.PlaceRef,
 
     if details.geometry_output & ntyp.GeometryFormat.GEOJSON:
         def _add_geometry(sql: SaSelect, column: SaColumn) -> SaSelect:
 
     if details.geometry_output & ntyp.GeometryFormat.GEOJSON:
         def _add_geometry(sql: SaSelect, column: SaColumn) -> SaSelect:
-            return sql.add_columns(sa.literal_column(f"""
-                      ST_AsGeoJSON(CASE WHEN ST_NPoints({column.name}) > 5000
-                                   THEN ST_SimplifyPreserveTopology({column.name}, 0.0001)
-                                   ELSE {column.name} END)
-                       """).label('geometry_geojson'))
+            return sql.add_columns(sa.func.ST_AsGeoJSON(
+                                    sa.case((sa.func.ST_NPoints(column) > 5000,
+                                             sa.func.ST_SimplifyPreserveTopology(column, 0.0001)),
+                                            else_=column)).label('geometry_geojson'))
     else:
         def _add_geometry(sql: SaSelect, column: SaColumn) -> SaSelect:
             return sql.add_columns(sa.func.ST_GeometryType(column).label('geometry_type'))
     else:
         def _add_geometry(sql: SaSelect, column: SaColumn) -> SaSelect:
             return sql.add_columns(sa.func.ST_GeometryType(column).label('geometry_type'))
@@ -183,6 +182,9 @@ async def get_detailed_place(conn: SearchConnection, place: ntyp.PlaceRef,
 
     # add missing details
     assert result is not None
 
     # add missing details
     assert result is not None
+    if 'type' in result.geometry:
+        result.geometry['type'] = GEOMETRY_TYPE_MAP.get(result.geometry['type'],
+                                                        result.geometry['type'])
     indexed_date = getattr(row, 'indexed_date', None)
     if indexed_date is not None:
         result.indexed_date = indexed_date.replace(tzinfo=dt.timezone.utc)
     indexed_date = getattr(row, 'indexed_date', None)
     if indexed_date is not None:
         result.indexed_date = indexed_date.replace(tzinfo=dt.timezone.utc)
@@ -236,3 +238,14 @@ async def get_simple_place(conn: SearchConnection, place: ntyp.PlaceRef,
     await nres.add_result_details(conn, [result], details)
 
     return result
     await nres.add_result_details(conn, [result], details)
 
     return result
+
+
+GEOMETRY_TYPE_MAP = {
+    'POINT': 'ST_Point',
+    'MULTIPOINT': 'ST_MultiPoint',
+    'LINESTRING': 'ST_LineString',
+    'MULTILINESTRING': 'ST_MultiLineString',
+    'POLYGON': 'ST_Polygon',
+    'MULTIPOLYGON': 'ST_MultiPolygon',
+    'GEOMETRYCOLLECTION': 'ST_GeometryCollection'
+}
index 166f501386fe3e33785064c4de08bbd3d6c86db4..469f06d98703a6a960d3eb26663cb5331a1bdb48 100644 (file)
@@ -19,7 +19,7 @@ import datetime as dt
 import sqlalchemy as sa
 
 from nominatim.typing import SaSelect, SaRow
 import sqlalchemy as sa
 
 from nominatim.typing import SaSelect, SaRow
-from nominatim.db.sqlalchemy_functions import CrosscheckNames
+from nominatim.db.sqlalchemy_types import Geometry
 from nominatim.api.types import Point, Bbox, LookupDetails
 from nominatim.api.connection import SearchConnection
 from nominatim.api.logging import log
 from nominatim.api.types import Point, Bbox, LookupDetails
 from nominatim.api.connection import SearchConnection
 from nominatim.api.logging import log
@@ -589,7 +589,7 @@ async def complete_address_details(conn: SearchConnection, results: List[BaseRes
     if not lookup_ids:
         return
 
     if not lookup_ids:
         return
 
-    ltab = sa.func.json_array_elements(sa.type_coerce(lookup_ids, sa.JSON))\
+    ltab = sa.func.JsonArrayEach(sa.type_coerce(lookup_ids, sa.JSON))\
              .table_valued(sa.column('value', type_=sa.JSON)) # type: ignore[no-untyped-call]
 
     t = conn.t.placex
              .table_valued(sa.column('value', type_=sa.JSON)) # type: ignore[no-untyped-call]
 
     t = conn.t.placex
@@ -608,7 +608,7 @@ async def complete_address_details(conn: SearchConnection, results: List[BaseRes
             .order_by('src_place_id')\
             .order_by(sa.column('rank_address').desc())\
             .order_by((taddr.c.place_id == ltab.c.value['pid'].as_integer()).desc())\
             .order_by('src_place_id')\
             .order_by(sa.column('rank_address').desc())\
             .order_by((taddr.c.place_id == ltab.c.value['pid'].as_integer()).desc())\
-            .order_by(sa.case((CrosscheckNames(t.c.name, ltab.c.value['names']), 2),
+            .order_by(sa.case((sa.func.CrosscheckNames(t.c.name, ltab.c.value['names']), 2),
                               (taddr.c.isaddress, 0),
                               (sa.and_(taddr.c.fromarea,
                                        t.c.geometry.ST_Contains(
                               (taddr.c.isaddress, 0),
                               (sa.and_(taddr.c.fromarea,
                                        t.c.geometry.ST_Contains(
@@ -652,7 +652,7 @@ async def complete_address_details(conn: SearchConnection, results: List[BaseRes
 
     parent_lookup_ids = list(filter(lambda e: e['pid'] != e['lid'], lookup_ids))
     if parent_lookup_ids:
 
     parent_lookup_ids = list(filter(lambda e: e['pid'] != e['lid'], lookup_ids))
     if parent_lookup_ids:
-        ltab = sa.func.json_array_elements(sa.type_coerce(parent_lookup_ids, sa.JSON))\
+        ltab = sa.func.JsonArrayEach(sa.type_coerce(parent_lookup_ids, sa.JSON))\
                  .table_valued(sa.column('value', type_=sa.JSON)) # type: ignore[no-untyped-call]
         sql = sa.select(ltab.c.value['pid'].as_integer().label('src_place_id'),
                         t.c.place_id, t.c.osm_type, t.c.osm_id, t.c.name,
                  .table_valued(sa.column('value', type_=sa.JSON)) # type: ignore[no-untyped-call]
         sql = sa.select(ltab.c.value['pid'].as_integer().label('src_place_id'),
                         t.c.place_id, t.c.osm_type, t.c.osm_id, t.c.name,
@@ -687,14 +687,10 @@ def _placex_select_address_row(conn: SearchConnection,
     return sa.select(t.c.place_id, t.c.osm_type, t.c.osm_id, t.c.name,
                      t.c.class_.label('class'), t.c.type,
                      t.c.admin_level, t.c.housenumber,
     return sa.select(t.c.place_id, t.c.osm_type, t.c.osm_id, t.c.name,
                      t.c.class_.label('class'), t.c.type,
                      t.c.admin_level, t.c.housenumber,
-                     sa.literal_column("""ST_GeometryType(geometry) in
-                                        ('ST_Polygon','ST_MultiPolygon')""").label('fromarea'),
+                     t.c.geometry.is_area().label('fromarea'),
                      t.c.rank_address,
                      t.c.rank_address,
-                     sa.literal_column(
-                         f"""ST_DistanceSpheroid(geometry,
-                                                 'SRID=4326;{centroid.to_wkt()}'::geometry,
-                              'SPHEROID["WGS 84",6378137,298.257223563, AUTHORITY["EPSG","7030"]]')
-                         """).label('distance'))
+                     t.c.geometry.distance_spheroid(
+                       sa.bindparam('centroid', value=centroid, type_=Geometry)).label('distance'))
 
 
 async def complete_linked_places(conn: SearchConnection, result: BaseResult) -> None:
 
 
 async def complete_linked_places(conn: SearchConnection, result: BaseResult) -> None:
@@ -728,10 +724,10 @@ async def complete_keywords(conn: SearchConnection, result: BaseResult) -> None:
     sel = sa.select(t.c.word_id, t.c.word_token, t.c.word)
 
     for name_tokens, address_tokens in await conn.execute(sql):
     sel = sa.select(t.c.word_id, t.c.word_token, t.c.word)
 
     for name_tokens, address_tokens in await conn.execute(sql):
-        for row in await conn.execute(sel.where(t.c.word_id == sa.any_(name_tokens))):
+        for row in await conn.execute(sel.where(t.c.word_id.in_(name_tokens))):
             result.name_keywords.append(WordInfo(*row))
 
             result.name_keywords.append(WordInfo(*row))
 
-        for row in await conn.execute(sel.where(t.c.word_id == sa.any_(address_tokens))):
+        for row in await conn.execute(sel.where(t.c.word_id.in_(address_tokens))):
             result.address_keywords.append(WordInfo(*row))
 
 
             result.address_keywords.append(WordInfo(*row))
 
 
index 064fa6a3d6d182d603950d2d7cb5f09c160d7d3e..f88c801e35d5a777abc075768658d7736d145a0f 100644 (file)
@@ -10,7 +10,6 @@ Custom functions and expressions for SQLAlchemy.
 from typing import Any
 
 import sqlalchemy as sa
 from typing import Any
 
 import sqlalchemy as sa
-from sqlalchemy.sql.expression import FunctionElement
 from sqlalchemy.ext.compiler import compiles
 
 from nominatim.typing import SaColumn
 from sqlalchemy.ext.compiler import compiles
 
 from nominatim.typing import SaColumn
@@ -41,10 +40,11 @@ def select_index_placex_geometry_reverse_lookupplacenode(table: str) -> 'sa.Text
                    f" AND {table}.osm_type = 'N'")
 
 
                    f" AND {table}.osm_type = 'N'")
 
 
-class CrosscheckNames(FunctionElement[Any]):
+class CrosscheckNames(sa.sql.functions.GenericFunction[bool]):
     """ Check if in the given list of names in parameters 1 any of the names
         from the JSON array in parameter 2 are contained.
     """
     """ Check if in the given list of names in parameters 1 any of the names
         from the JSON array in parameter 2 are contained.
     """
+    type = sa.Boolean()
     name = 'CrosscheckNames'
     inherit_cache = True
 
     name = 'CrosscheckNames'
     inherit_cache = True
 
@@ -54,3 +54,42 @@ def compile_crosscheck_names(element: SaColumn,
     arg1, arg2 = list(element.clauses)
     return "coalesce(avals(%s) && ARRAY(SELECT * FROM json_array_elements_text(%s)), false)" % (
             compiler.process(arg1, **kw), compiler.process(arg2, **kw))
     arg1, arg2 = list(element.clauses)
     return "coalesce(avals(%s) && ARRAY(SELECT * FROM json_array_elements_text(%s)), false)" % (
             compiler.process(arg1, **kw), compiler.process(arg2, **kw))
+
+
+@compiles(CrosscheckNames, 'sqlite') # type: ignore[no-untyped-call, misc]
+def compile_sqlite_crosscheck_names(element: SaColumn,
+                                    compiler: 'sa.Compiled', **kw: Any) -> str:
+    arg1, arg2 = list(element.clauses)
+    return "EXISTS(SELECT *"\
+           " FROM json_each(%s) as name, json_each(%s) as match_name"\
+           " WHERE name.value = match_name.value)"\
+           % (compiler.process(arg1, **kw), compiler.process(arg2, **kw))
+
+
+class JsonArrayEach(sa.sql.functions.GenericFunction[Any]):
+    """ Return elements of a json array as a set.
+    """
+    name = 'JsonArrayEach'
+    inherit_cache = True
+
+
+@compiles(JsonArrayEach) # type: ignore[no-untyped-call, misc]
+def default_json_array_each(element: SaColumn, compiler: 'sa.Compiled', **kw: Any) -> str:
+    return "json_array_elements(%s)" % compiler.process(element.clauses, **kw)
+
+
+@compiles(JsonArrayEach, 'sqlite') # type: ignore[no-untyped-call, misc]
+def sqlite_json_array_each(element: SaColumn, compiler: 'sa.Compiled', **kw: Any) -> str:
+    return "json_each(%s)" % compiler.process(element.clauses, **kw)
+
+
+class Greatest(sa.sql.functions.GenericFunction[Any]):
+    """ Function to compute maximum of all its input parameters.
+    """
+    name = 'greatest'
+    inherit_cache = True
+
+
+@compiles(Greatest, 'sqlite') # type: ignore[no-untyped-call, misc]
+def sqlite_greatest(element: SaColumn, compiler: 'sa.Compiled', **kw: Any) -> str:
+    return "max(%s)" % compiler.process(element.clauses, **kw)
index 9d1e48fae31e3c1763a28abe43ce13a37e1dd03c..8e8cc9c8aa47c90d2fc6c4e6291c62c083df21ec 100644 (file)
@@ -18,29 +18,26 @@ from nominatim.typing import SaColumn, SaBind
 
 #pylint: disable=all
 
 
 #pylint: disable=all
 
-SQLITE_FUNCTION_ALIAS = (
-    ('ST_AsEWKB', sa.Text, 'AsEWKB'),
-    ('ST_AsGeoJSON', sa.Text, 'AsGeoJSON'),
-    ('ST_AsKML', sa.Text, 'AsKML'),
-    ('ST_AsSVG', sa.Text, 'AsSVG'),
-)
-
-def _add_function_alias(func: str, ftype: type, alias: str) -> None:
-    _FuncDef = type(func, (sa.sql.functions.GenericFunction, ), {
-        "type": ftype,
-        "name": func,
-        "identifier": func,
-        "inherit_cache": True})
+class Geometry_DistanceSpheroid(sa.sql.expression.FunctionElement[float]):
+    """ Function to compute the spherical distance in meters.
+    """
+    type = sa.Float()
+    name = 'Geometry_DistanceSpheroid'
+    inherit_cache = True
 
 
-    func_templ = f"{alias}(%s)"
 
 
-    def _sqlite_impl(element: Any, compiler: Any, **kw: Any) -> Any:
-        return func_templ % compiler.process(element.clauses, **kw)
+@compiles(Geometry_DistanceSpheroid) # type: ignore[no-untyped-call, misc]
+def _default_distance_spheroid(element: SaColumn,
+                               compiler: 'sa.Compiled', **kw: Any) -> str:
+    return "ST_DistanceSpheroid(%s,"\
+           " 'SPHEROID[\"WGS 84\",6378137,298.257223563, AUTHORITY[\"EPSG\",\"7030\"]]')"\
+             % compiler.process(element.clauses, **kw)
 
 
-    compiles(_FuncDef, 'sqlite')(_sqlite_impl) # type: ignore[no-untyped-call]
 
 
-for alias in SQLITE_FUNCTION_ALIAS:
-    _add_function_alias(*alias)
+@compiles(Geometry_DistanceSpheroid, 'sqlite') # type: ignore[no-untyped-call, misc]
+def _spatialite_distance_spheroid(element: SaColumn,
+                                  compiler: 'sa.Compiled', **kw: Any) -> str:
+    return "Distance(%s, true)" % compiler.process(element.clauses, **kw)
 
 
 class Geometry(types.UserDefinedType): # type: ignore[type-arg]
 
 
 class Geometry(types.UserDefinedType): # type: ignore[type-arg]
@@ -148,6 +145,39 @@ class Geometry(types.UserDefinedType): # type: ignore[type-arg]
             return sa.func.ST_LineLocatePoint(self, other, type_=sa.Float)
 
 
             return sa.func.ST_LineLocatePoint(self, other, type_=sa.Float)
 
 
+        def distance_spheroid(self, other: SaColumn) -> SaColumn:
+            return Geometry_DistanceSpheroid(self, other)
+
+
 @compiles(Geometry, 'sqlite') # type: ignore[no-untyped-call]
 def get_col_spec(self, *args, **kwargs): # type: ignore[no-untyped-def]
     return 'GEOMETRY'
 @compiles(Geometry, 'sqlite') # type: ignore[no-untyped-call]
 def get_col_spec(self, *args, **kwargs): # type: ignore[no-untyped-def]
     return 'GEOMETRY'
+
+
+SQLITE_FUNCTION_ALIAS = (
+    ('ST_AsEWKB', sa.Text, 'AsEWKB'),
+    ('ST_GeomFromEWKT', Geometry, 'GeomFromEWKT'),
+    ('ST_AsGeoJSON', sa.Text, 'AsGeoJSON'),
+    ('ST_AsKML', sa.Text, 'AsKML'),
+    ('ST_AsSVG', sa.Text, 'AsSVG'),
+)
+
+def _add_function_alias(func: str, ftype: type, alias: str) -> None:
+    _FuncDef = type(func, (sa.sql.functions.GenericFunction, ), {
+        "type": ftype,
+        "name": func,
+        "identifier": func,
+        "inherit_cache": True})
+
+    func_templ = f"{alias}(%s)"
+
+    def _sqlite_impl(element: Any, compiler: Any, **kw: Any) -> Any:
+        return func_templ % compiler.process(element.clauses, **kw)
+
+    compiles(_FuncDef, 'sqlite')(_sqlite_impl) # type: ignore[no-untyped-call]
+
+for alias in SQLITE_FUNCTION_ALIAS:
+    _add_function_alias(*alias)
+
+
+
index ca14b93c178e60cbd019cd667922f5fda71c02ed..596876d4503dac42d94ab8a06d4cd3f74154a41c 100644 (file)
@@ -15,7 +15,7 @@ import nominatim.api as napi
 
 @pytest.mark.parametrize('idobj', (napi.PlaceID(332), napi.OsmID('W', 4),
                                    napi.OsmID('W', 4, 'highway')))
 
 @pytest.mark.parametrize('idobj', (napi.PlaceID(332), napi.OsmID('W', 4),
                                    napi.OsmID('W', 4, 'highway')))
-def test_lookup_in_placex(apiobj, idobj):
+def test_lookup_in_placex(apiobj, frontend, idobj):
     import_date = dt.datetime(2022, 12, 7, 14, 14, 46, 0)
     apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
                      class_='highway', type='residential',
     import_date = dt.datetime(2022, 12, 7, 14, 14, 46, 0)
     apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
                      class_='highway', type='residential',
@@ -31,7 +31,8 @@ def test_lookup_in_placex(apiobj, idobj):
                      indexed_date=import_date,
                      geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)')
 
                      indexed_date=import_date,
                      geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)')
 
-    result = apiobj.api.details(idobj)
+    api = frontend(apiobj, options={'details'})
+    result = api.details(idobj)
 
     assert result is not None
 
 
     assert result is not None
 
@@ -69,7 +70,7 @@ def test_lookup_in_placex(apiobj, idobj):
     assert result.geometry == {'type': 'ST_LineString'}
 
 
     assert result.geometry == {'type': 'ST_LineString'}
 
 
-def test_lookup_in_placex_minimal_info(apiobj):
+def test_lookup_in_placex_minimal_info(apiobj, frontend):
     import_date = dt.datetime(2022, 12, 7, 14, 14, 46, 0)
     apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
                      class_='highway', type='residential',
     import_date = dt.datetime(2022, 12, 7, 14, 14, 46, 0)
     apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
                      class_='highway', type='residential',
@@ -79,7 +80,8 @@ def test_lookup_in_placex_minimal_info(apiobj):
                      indexed_date=import_date,
                      geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)')
 
                      indexed_date=import_date,
                      geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)')
 
-    result = apiobj.api.details(napi.PlaceID(332))
+    api = frontend(apiobj, options={'details'})
+    result = api.details(napi.PlaceID(332))
 
     assert result is not None
 
 
     assert result is not None
 
@@ -117,16 +119,17 @@ def test_lookup_in_placex_minimal_info(apiobj):
     assert result.geometry == {'type': 'ST_LineString'}
 
 
     assert result.geometry == {'type': 'ST_LineString'}
 
 
-def test_lookup_in_placex_with_geometry(apiobj):
+def test_lookup_in_placex_with_geometry(apiobj, frontend):
     apiobj.add_placex(place_id=332,
                       geometry='LINESTRING(23 34, 23.1 34)')
 
     apiobj.add_placex(place_id=332,
                       geometry='LINESTRING(23 34, 23.1 34)')
 
-    result = apiobj.api.details(napi.PlaceID(332), geometry_output=napi.GeometryFormat.GEOJSON)
+    api = frontend(apiobj, options={'details'})
+    result = api.details(napi.PlaceID(332), geometry_output=napi.GeometryFormat.GEOJSON)
 
     assert result.geometry == {'geojson': '{"type":"LineString","coordinates":[[23,34],[23.1,34]]}'}
 
 
 
     assert result.geometry == {'geojson': '{"type":"LineString","coordinates":[[23,34],[23.1,34]]}'}
 
 
-def test_lookup_placex_with_address_details(apiobj):
+def test_lookup_placex_with_address_details(apiobj, frontend):
     apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
                      class_='highway', type='residential',  name='Street',
                      country_code='pl',
     apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
                      class_='highway', type='residential',  name='Street',
                      country_code='pl',
@@ -143,7 +146,8 @@ def test_lookup_placex_with_address_details(apiobj):
                               country_code='pl',
                               rank_search=17, rank_address=16)
 
                               country_code='pl',
                               rank_search=17, rank_address=16)
 
-    result = apiobj.api.details(napi.PlaceID(332), address_details=True)
+    api = frontend(apiobj, options={'details'})
+    result = api.details(napi.PlaceID(332), address_details=True)
 
     assert result.address_rows == [
                napi.AddressLine(place_id=332, osm_object=('W', 4),
 
     assert result.address_rows == [
                napi.AddressLine(place_id=332, osm_object=('W', 4),
@@ -172,18 +176,19 @@ def test_lookup_placex_with_address_details(apiobj):
            ]
 
 
            ]
 
 
-def test_lookup_place_with_linked_places_none_existing(apiobj):
+def test_lookup_place_with_linked_places_none_existing(apiobj, frontend):
     apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
                      class_='highway', type='residential',  name='Street',
                      country_code='pl', linked_place_id=45,
                      rank_search=27, rank_address=26)
 
     apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
                      class_='highway', type='residential',  name='Street',
                      country_code='pl', linked_place_id=45,
                      rank_search=27, rank_address=26)
 
-    result = apiobj.api.details(napi.PlaceID(332), linked_places=True)
+    api = frontend(apiobj, options={'details'})
+    result = api.details(napi.PlaceID(332), linked_places=True)
 
     assert result.linked_rows == []
 
 
 
     assert result.linked_rows == []
 
 
-def test_lookup_place_with_linked_places_existing(apiobj):
+def test_lookup_place_with_linked_places_existing(apiobj, frontend):
     apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
                      class_='highway', type='residential',  name='Street',
                      country_code='pl', linked_place_id=45,
     apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
                      class_='highway', type='residential',  name='Street',
                      country_code='pl', linked_place_id=45,
@@ -197,7 +202,8 @@ def test_lookup_place_with_linked_places_existing(apiobj):
                      country_code='pl', linked_place_id=332,
                      rank_search=27, rank_address=26)
 
                      country_code='pl', linked_place_id=332,
                      rank_search=27, rank_address=26)
 
-    result = apiobj.api.details(napi.PlaceID(332), linked_places=True)
+    api = frontend(apiobj, options={'details'})
+    result = api.details(napi.PlaceID(332), linked_places=True)
 
     assert result.linked_rows == [
                napi.AddressLine(place_id=1001, osm_object=('W', 5),
 
     assert result.linked_rows == [
                napi.AddressLine(place_id=1001, osm_object=('W', 5),
@@ -213,18 +219,19 @@ def test_lookup_place_with_linked_places_existing(apiobj):
     ]
 
 
     ]
 
 
-def test_lookup_place_with_parented_places_not_existing(apiobj):
+def test_lookup_place_with_parented_places_not_existing(apiobj, frontend):
     apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
                      class_='highway', type='residential',  name='Street',
                      country_code='pl', parent_place_id=45,
                      rank_search=27, rank_address=26)
 
     apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
                      class_='highway', type='residential',  name='Street',
                      country_code='pl', parent_place_id=45,
                      rank_search=27, rank_address=26)
 
-    result = apiobj.api.details(napi.PlaceID(332), parented_places=True)
+    api = frontend(apiobj, options={'details'})
+    result = api.details(napi.PlaceID(332), parented_places=True)
 
     assert result.parented_rows == []
 
 
 
     assert result.parented_rows == []
 
 
-def test_lookup_place_with_parented_places_existing(apiobj):
+def test_lookup_place_with_parented_places_existing(apiobj, frontend):
     apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
                      class_='highway', type='residential',  name='Street',
                      country_code='pl', parent_place_id=45,
     apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
                      class_='highway', type='residential',  name='Street',
                      country_code='pl', parent_place_id=45,
@@ -238,7 +245,8 @@ def test_lookup_place_with_parented_places_existing(apiobj):
                      country_code='pl', parent_place_id=332,
                      rank_search=27, rank_address=26)
 
                      country_code='pl', parent_place_id=332,
                      rank_search=27, rank_address=26)
 
-    result = apiobj.api.details(napi.PlaceID(332), parented_places=True)
+    api = frontend(apiobj, options={'details'})
+    result = api.details(napi.PlaceID(332), parented_places=True)
 
     assert result.parented_rows == [
                napi.AddressLine(place_id=1001, osm_object=('N', 5),
 
     assert result.parented_rows == [
                napi.AddressLine(place_id=1001, osm_object=('N', 5),
@@ -250,7 +258,7 @@ def test_lookup_place_with_parented_places_existing(apiobj):
 
 
 @pytest.mark.parametrize('idobj', (napi.PlaceID(4924), napi.OsmID('W', 9928)))
 
 
 @pytest.mark.parametrize('idobj', (napi.PlaceID(4924), napi.OsmID('W', 9928)))
-def test_lookup_in_osmline(apiobj, idobj):
+def test_lookup_in_osmline(apiobj, frontend, idobj):
     import_date = dt.datetime(2022, 12, 7, 14, 14, 46, 0)
     apiobj.add_osmline(place_id=4924, osm_id=9928,
                        parent_place_id=12,
     import_date = dt.datetime(2022, 12, 7, 14, 14, 46, 0)
     apiobj.add_osmline(place_id=4924, osm_id=9928,
                        parent_place_id=12,
@@ -260,7 +268,8 @@ def test_lookup_in_osmline(apiobj, idobj):
                        indexed_date=import_date,
                        geometry='LINESTRING(23 34, 23 35)')
 
                        indexed_date=import_date,
                        geometry='LINESTRING(23 34, 23 35)')
 
-    result = apiobj.api.details(idobj)
+    api = frontend(apiobj, options={'details'})
+    result = api.details(idobj)
 
     assert result is not None
 
 
     assert result is not None
 
@@ -298,7 +307,7 @@ def test_lookup_in_osmline(apiobj, idobj):
     assert result.geometry == {'type': 'ST_LineString'}
 
 
     assert result.geometry == {'type': 'ST_LineString'}
 
 
-def test_lookup_in_osmline_split_interpolation(apiobj):
+def test_lookup_in_osmline_split_interpolation(apiobj, frontend):
     apiobj.add_osmline(place_id=1000, osm_id=9,
                        startnumber=2, endnumber=4, step=1)
     apiobj.add_osmline(place_id=1001, osm_id=9,
     apiobj.add_osmline(place_id=1000, osm_id=9,
                        startnumber=2, endnumber=4, step=1)
     apiobj.add_osmline(place_id=1001, osm_id=9,
@@ -306,18 +315,19 @@ def test_lookup_in_osmline_split_interpolation(apiobj):
     apiobj.add_osmline(place_id=1002, osm_id=9,
                        startnumber=11, endnumber=20, step=1)
 
     apiobj.add_osmline(place_id=1002, osm_id=9,
                        startnumber=11, endnumber=20, step=1)
 
+    api = frontend(apiobj, options={'details'})
     for i in range(1, 6):
     for i in range(1, 6):
-        result = apiobj.api.details(napi.OsmID('W', 9, str(i)))
+        result = api.details(napi.OsmID('W', 9, str(i)))
         assert result.place_id == 1000
     for i in range(7, 11):
         assert result.place_id == 1000
     for i in range(7, 11):
-        result = apiobj.api.details(napi.OsmID('W', 9, str(i)))
+        result = api.details(napi.OsmID('W', 9, str(i)))
         assert result.place_id == 1001
     for i in range(12, 22):
         assert result.place_id == 1001
     for i in range(12, 22):
-        result = apiobj.api.details(napi.OsmID('W', 9, str(i)))
+        result = api.details(napi.OsmID('W', 9, str(i)))
         assert result.place_id == 1002
 
 
         assert result.place_id == 1002
 
 
-def test_lookup_osmline_with_address_details(apiobj):
+def test_lookup_osmline_with_address_details(apiobj, frontend):
     apiobj.add_osmline(place_id=9000, osm_id=9,
                        startnumber=2, endnumber=4, step=1,
                        parent_place_id=332)
     apiobj.add_osmline(place_id=9000, osm_id=9,
                        startnumber=2, endnumber=4, step=1,
                        parent_place_id=332)
@@ -337,7 +347,8 @@ def test_lookup_osmline_with_address_details(apiobj):
                               country_code='pl',
                               rank_search=17, rank_address=16)
 
                               country_code='pl',
                               rank_search=17, rank_address=16)
 
-    result = apiobj.api.details(napi.PlaceID(9000), address_details=True)
+    api = frontend(apiobj, options={'details'})
+    result = api.details(napi.PlaceID(9000), address_details=True)
 
     assert result.address_rows == [
                napi.AddressLine(place_id=332, osm_object=('W', 4),
 
     assert result.address_rows == [
                napi.AddressLine(place_id=332, osm_object=('W', 4),
@@ -366,7 +377,7 @@ def test_lookup_osmline_with_address_details(apiobj):
            ]
 
 
            ]
 
 
-def test_lookup_in_tiger(apiobj):
+def test_lookup_in_tiger(apiobj, frontend):
     apiobj.add_tiger(place_id=4924,
                      parent_place_id=12,
                      startnumber=1, endnumber=4, step=1,
     apiobj.add_tiger(place_id=4924,
                      parent_place_id=12,
                      startnumber=1, endnumber=4, step=1,
@@ -377,7 +388,8 @@ def test_lookup_in_tiger(apiobj):
                       osm_type='W', osm_id=6601223,
                       geometry='LINESTRING(23 34, 23 35)')
 
                       osm_type='W', osm_id=6601223,
                       geometry='LINESTRING(23 34, 23 35)')
 
-    result = apiobj.api.details(napi.PlaceID(4924))
+    api = frontend(apiobj, options={'details'})
+    result = api.details(napi.PlaceID(4924))
 
     assert result is not None
 
 
     assert result is not None
 
@@ -415,7 +427,7 @@ def test_lookup_in_tiger(apiobj):
     assert result.geometry == {'type': 'ST_LineString'}
 
 
     assert result.geometry == {'type': 'ST_LineString'}
 
 
-def test_lookup_tiger_with_address_details(apiobj):
+def test_lookup_tiger_with_address_details(apiobj, frontend):
     apiobj.add_tiger(place_id=9000,
                      startnumber=2, endnumber=4, step=1,
                      parent_place_id=332)
     apiobj.add_tiger(place_id=9000,
                      startnumber=2, endnumber=4, step=1,
                      parent_place_id=332)
@@ -435,7 +447,8 @@ def test_lookup_tiger_with_address_details(apiobj):
                               country_code='us',
                               rank_search=17, rank_address=16)
 
                               country_code='us',
                               rank_search=17, rank_address=16)
 
-    result = apiobj.api.details(napi.PlaceID(9000), address_details=True)
+    api = frontend(apiobj, options={'details'})
+    result = api.details(napi.PlaceID(9000), address_details=True)
 
     assert result.address_rows == [
                napi.AddressLine(place_id=332, osm_object=('W', 4),
 
     assert result.address_rows == [
                napi.AddressLine(place_id=332, osm_object=('W', 4),
@@ -464,7 +477,7 @@ def test_lookup_tiger_with_address_details(apiobj):
            ]
 
 
            ]
 
 
-def test_lookup_in_postcode(apiobj):
+def test_lookup_in_postcode(apiobj, frontend):
     import_date = dt.datetime(2022, 12, 7, 14, 14, 46, 0)
     apiobj.add_postcode(place_id=554,
                         parent_place_id=152,
     import_date = dt.datetime(2022, 12, 7, 14, 14, 46, 0)
     apiobj.add_postcode(place_id=554,
                         parent_place_id=152,
@@ -474,7 +487,8 @@ def test_lookup_in_postcode(apiobj):
                         indexed_date=import_date,
                         geometry='POINT(-9.45 5.6)')
 
                         indexed_date=import_date,
                         geometry='POINT(-9.45 5.6)')
 
-    result = apiobj.api.details(napi.PlaceID(554))
+    api = frontend(apiobj, options={'details'})
+    result = api.details(napi.PlaceID(554))
 
     assert result is not None
 
 
     assert result is not None
 
@@ -512,7 +526,7 @@ def test_lookup_in_postcode(apiobj):
     assert result.geometry == {'type': 'ST_Point'}
 
 
     assert result.geometry == {'type': 'ST_Point'}
 
 
-def test_lookup_postcode_with_address_details(apiobj):
+def test_lookup_postcode_with_address_details(apiobj, frontend):
     apiobj.add_postcode(place_id=9000,
                         parent_place_id=332,
                         postcode='34 425',
     apiobj.add_postcode(place_id=9000,
                         parent_place_id=332,
                         postcode='34 425',
@@ -528,7 +542,8 @@ def test_lookup_postcode_with_address_details(apiobj):
                               country_code='gb',
                               rank_search=17, rank_address=16)
 
                               country_code='gb',
                               rank_search=17, rank_address=16)
 
-    result = apiobj.api.details(napi.PlaceID(9000), address_details=True)
+    api = frontend(apiobj, options={'details'})
+    result = api.details(napi.PlaceID(9000), address_details=True)
 
     assert result.address_rows == [
                napi.AddressLine(place_id=9000, osm_object=None,
 
     assert result.address_rows == [
                napi.AddressLine(place_id=9000, osm_object=None,
@@ -559,18 +574,20 @@ def test_lookup_postcode_with_address_details(apiobj):
 @pytest.mark.parametrize('objid', [napi.PlaceID(1736),
                                    napi.OsmID('W', 55),
                                    napi.OsmID('N', 55, 'amenity')])
 @pytest.mark.parametrize('objid', [napi.PlaceID(1736),
                                    napi.OsmID('W', 55),
                                    napi.OsmID('N', 55, 'amenity')])
-def test_lookup_missing_object(apiobj, objid):
+def test_lookup_missing_object(apiobj, frontend, objid):
     apiobj.add_placex(place_id=1, osm_type='N', osm_id=55,
                       class_='place', type='suburb')
 
     apiobj.add_placex(place_id=1, osm_type='N', osm_id=55,
                       class_='place', type='suburb')
 
-    assert apiobj.api.details(objid) is None
+    api = frontend(apiobj, options={'details'})
+    assert api.details(objid) is None
 
 
 @pytest.mark.parametrize('gtype', (napi.GeometryFormat.KML,
                                     napi.GeometryFormat.SVG,
                                     napi.GeometryFormat.TEXT))
 
 
 @pytest.mark.parametrize('gtype', (napi.GeometryFormat.KML,
                                     napi.GeometryFormat.SVG,
                                     napi.GeometryFormat.TEXT))
-def test_lookup_unsupported_geometry(apiobj, gtype):
+def test_lookup_unsupported_geometry(apiobj, frontend, gtype):
     apiobj.add_placex(place_id=332)
 
     apiobj.add_placex(place_id=332)
 
+    api = frontend(apiobj, options={'details'})
     with pytest.raises(ValueError):
     with pytest.raises(ValueError):
-        apiobj.api.details(napi.PlaceID(332), geometry_output=gtype)
+        api.details(napi.PlaceID(332), geometry_output=gtype)