]> git.openstreetmap.org Git - nominatim.git/commitdiff
Merge remote-tracking branch 'upstream/master'
authorSarah Hoffmann <lonvia@denofr.de>
Fri, 7 Apr 2023 12:04:58 +0000 (14:04 +0200)
committerSarah Hoffmann <lonvia@denofr.de>
Fri, 7 Apr 2023 12:04:58 +0000 (14:04 +0200)
17 files changed:
lib-sql/functions/interpolation.sql
lib-sql/functions/utils.sql
nominatim/api/__init__.py
nominatim/api/core.py
nominatim/api/lookup.py
nominatim/api/results.py
nominatim/api/v1/classtypes.py
nominatim/api/v1/format.py
nominatim/api/v1/format_json.py
nominatim/api/v1/format_xml.py
nominatim/api/v1/server_glue.py
nominatim/clicmd/api.py
test/bdd/db/import/interpolation.feature
test/python/api/test_api_details.py [new file with mode: 0644]
test/python/api/test_api_lookup.py
test/python/api/test_server_glue_v1.py
test/python/cli/test_cmd_api.py

index 9bb9102172b81169028ba302a52fa8d7b7676a0b..928d55c546294bb37b7da710473425442c524a59 100644 (file)
@@ -164,7 +164,7 @@ DECLARE
   newend INTEGER;
   moddiff SMALLINT;
   linegeo GEOMETRY;
-  splitline GEOMETRY;
+  splitpoint FLOAT;
   sectiongeo GEOMETRY;
   postcode TEXT;
   stepmod SMALLINT;
@@ -223,15 +223,27 @@ BEGIN
         FROM placex, generate_series(1, array_upper(waynodes, 1)) nodeidpos
         WHERE osm_type = 'N' and osm_id = waynodes[nodeidpos]::BIGINT
               and address is not NULL and address ? 'housenumber'
+              and ST_Distance(NEW.linegeo, geometry) < 0.0005
         ORDER BY nodeidpos
     LOOP
       {% if debug %}RAISE WARNING 'processing point % (%)', nextnode.hnr, ST_AsText(nextnode.geometry);{% endif %}
       IF linegeo is null THEN
         linegeo := NEW.linegeo;
       ELSE
-        splitline := ST_Split(ST_Snap(linegeo, nextnode.geometry, 0.0005), nextnode.geometry);
-        sectiongeo := ST_GeometryN(splitline, 1);
-        linegeo := ST_GeometryN(splitline, 2);
+        splitpoint := ST_LineLocatePoint(linegeo, nextnode.geometry);
+        IF splitpoint = 0 THEN
+          -- Corner case where the splitpoint falls on the first point
+          -- and thus would not return a geometry. Skip that section.
+          sectiongeo := NULL;
+        ELSEIF splitpoint = 1 THEN
+          -- Point is at the end of the line.
+          sectiongeo := linegeo;
+          linegeo := NULL;
+        ELSE
+          -- Split the line.
+          sectiongeo := ST_LineSubstring(linegeo, 0, splitpoint);
+          linegeo := ST_LineSubstring(linegeo, splitpoint, 1);
+        END IF;
       END IF;
 
       IF prevnode.hnr is not null
@@ -239,6 +251,9 @@ BEGIN
          -- regularly mapped housenumbers.
          -- (Conveniently also fails if one of the house numbers is not a number.)
          and abs(prevnode.hnr - nextnode.hnr) > NEW.step
+         -- If the interpolation geometry is broken or two nodes are at the
+         -- same place, then splitting might produce a point. Ignore that.
+         and ST_GeometryType(sectiongeo) = 'ST_LineString'
       THEN
         IF prevnode.hnr < nextnode.hnr THEN
           startnumber := prevnode.hnr;
@@ -300,12 +315,12 @@ BEGIN
                   NEW.address, postcode,
                   NEW.country_code, NEW.geometry_sector, 0);
         END IF;
+      END IF;
 
-        -- early break if we are out of line string,
-        -- might happen when a line string loops back on itself
-        IF ST_GeometryType(linegeo) != 'ST_LineString' THEN
-            RETURN NEW;
-        END IF;
+      -- early break if we are out of line string,
+      -- might happen when a line string loops back on itself
+      IF linegeo is null or ST_GeometryType(linegeo) != 'ST_LineString' THEN
+          RETURN NEW;
       END IF;
 
       prevnode := nextnode;
index ad262670092760521bfa677ba9a40ac4d1353037..f5be7b61262532d462044ff7657a4bf69ee7ae58 100644 (file)
@@ -429,9 +429,10 @@ BEGIN
   SELECT osm_type, osm_id, class, type FROM placex WHERE place_id = placeid INTO osmtype, osmid, pclass, ptype;
   DELETE FROM import_polygon_delete where osm_type = osmtype and osm_id = osmid and class = pclass and type = ptype;
   DELETE FROM import_polygon_error where osm_type = osmtype and osm_id = osmid and class = pclass and type = ptype;
-  -- force delete from place/placex by making it a very small geometry
-  UPDATE place set geometry = ST_SetSRID(ST_Point(0,0), 4326) where osm_type = osmtype and osm_id = osmid and class = pclass and type = ptype;
-  DELETE FROM place where osm_type = osmtype and osm_id = osmid and class = pclass and type = ptype;
+  -- force delete by directly entering it into the to-be-deleted table
+  INSERT INTO place_to_be_deleted (osm_type, osm_id, class, type, deferred)
+         VALUES(osmtype, osmid, pclass, ptype, false);
+  PERFORM flush_deleted_places();
 
   RETURN TRUE;
 END;
index 0a91e28185b33a4c4bcfa51200e7a55fbec7540d..9f3623799ac9c763c43ab6389f7818dab97e6c74 100644 (file)
@@ -32,5 +32,7 @@ from .results import (SourceTable as SourceTable,
                       WordInfos as WordInfos,
                       DetailedResult as DetailedResult,
                       ReverseResult as ReverseResult,
-                      ReverseResults as ReverseResults)
+                      ReverseResults as ReverseResults,
+                      SearchResult as SearchResult,
+                      SearchResults as SearchResults)
 from .localization import (Locales as Locales)
index 6d47d332371fffd1cbfae42deecfe695848e04ec..29325b08089603106cb6dfae44dd731ae0908727 100644 (file)
@@ -7,7 +7,7 @@
 """
 Implementation of classes for API access via libraries.
 """
-from typing import Mapping, Optional, Any, AsyncIterator, Dict
+from typing import Mapping, Optional, Any, AsyncIterator, Dict, Sequence
 import asyncio
 import contextlib
 from pathlib import Path
@@ -20,10 +20,10 @@ from nominatim.db.sqlalchemy_schema import SearchTables
 from nominatim.config import Configuration
 from nominatim.api.connection import SearchConnection
 from nominatim.api.status import get_status, StatusResult
-from nominatim.api.lookup import get_place_by_id
+from nominatim.api.lookup import get_detailed_place, get_simple_place
 from nominatim.api.reverse import ReverseGeocoder
 from nominatim.api.types import PlaceRef, LookupDetails, AnyPoint, DataLayer
-from nominatim.api.results import DetailedResult, ReverseResult
+from nominatim.api.results import DetailedResult, ReverseResult, SearchResults
 
 
 class NominatimAPIAsync:
@@ -130,14 +130,27 @@ class NominatimAPIAsync:
         return status
 
 
-    async def lookup(self, place: PlaceRef,
-                     details: Optional[LookupDetails] = None) -> Optional[DetailedResult]:
+    async def details(self, place: PlaceRef,
+                      details: Optional[LookupDetails] = None) -> Optional[DetailedResult]:
         """ Get detailed information about a place in the database.
 
             Returns None if there is no entry under the given ID.
         """
         async with self.begin() as conn:
-            return await get_place_by_id(conn, place, details or LookupDetails())
+            return await get_detailed_place(conn, place, details or LookupDetails())
+
+
+    async def lookup(self, places: Sequence[PlaceRef],
+                      details: Optional[LookupDetails] = None) -> SearchResults:
+        """ Get simple information about a list of places.
+
+            Returns a list of place information for all IDs that were found.
+        """
+        if details is None:
+            details = LookupDetails()
+        async with self.begin() as conn:
+            return SearchResults(filter(None,
+                                        [await get_simple_place(conn, p, details) for p in places]))
 
 
     async def reverse(self, coord: AnyPoint, max_rank: Optional[int] = None,
@@ -195,11 +208,20 @@ class NominatimAPI:
         return self._loop.run_until_complete(self._async_api.status())
 
 
-    def lookup(self, place: PlaceRef,
-               details: Optional[LookupDetails] = None) -> Optional[DetailedResult]:
+    def details(self, place: PlaceRef,
+                details: Optional[LookupDetails] = None) -> Optional[DetailedResult]:
         """ Get detailed information about a place in the database.
         """
-        return self._loop.run_until_complete(self._async_api.lookup(place, details))
+        return self._loop.run_until_complete(self._async_api.details(place, details))
+
+
+    def lookup(self, places: Sequence[PlaceRef],
+               details: Optional[LookupDetails] = None) -> SearchResults:
+        """ Get simple information about a list of places.
+
+            Returns a list of place information for all IDs that were found.
+        """
+        return self._loop.run_until_complete(self._async_api.lookup(places, details))
 
 
     def reverse(self, coord: AnyPoint, max_rank: Optional[int] = None,
index 1b0ee87f29607fe66c48a9237b253861d6ae52e4..823527025d59c65baa729d620cd95ca10ab50d72 100644 (file)
@@ -7,34 +7,25 @@
 """
 Implementation of place lookup by ID.
 """
-from typing import Optional
+from typing import Optional, Callable, Tuple, Type
 import datetime as dt
 
 import sqlalchemy as sa
 
-from nominatim.typing import SaColumn, SaLabel, SaRow
+from nominatim.typing import SaColumn, SaRow, SaSelect
 from nominatim.api.connection import SearchConnection
 import nominatim.api.types as ntyp
 import nominatim.api.results as nres
 from nominatim.api.logging import log
 
-def _select_column_geometry(column: SaColumn,
-                            geometry_output: ntyp.GeometryFormat) -> SaLabel:
-    """ Create the appropriate column expression for selecting a
-        geometry for the details response.
-    """
-    if geometry_output & ntyp.GeometryFormat.GEOJSON:
-        return 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')
+RowFunc = Callable[[Optional[SaRow], Type[nres.BaseResultT]], Optional[nres.BaseResultT]]
+
+GeomFunc = Callable[[SaSelect, SaColumn], SaSelect]
 
-    return sa.func.ST_GeometryType(column).label('geometry_type')
 
 
 async def find_in_placex(conn: SearchConnection, place: ntyp.PlaceRef,
-                         details: ntyp.LookupDetails) -> Optional[SaRow]:
+                         add_geometries: GeomFunc) -> Optional[SaRow]:
     """ Search for the given place in the placex table and return the
         base information.
     """
@@ -47,8 +38,7 @@ async def find_in_placex(conn: SearchConnection, place: ntyp.PlaceRef,
                     t.c.importance, t.c.wikipedia, t.c.indexed_date,
                     t.c.parent_place_id, t.c.rank_address, t.c.rank_search,
                     t.c.linked_place_id,
-                    t.c.centroid,
-                    _select_column_geometry(t.c.geometry, details.geometry_output))
+                    t.c.centroid)
 
     if isinstance(place, ntyp.PlaceID):
         sql = sql.where(t.c.place_id == place.place_id)
@@ -63,11 +53,11 @@ async def find_in_placex(conn: SearchConnection, place: ntyp.PlaceRef,
     else:
         return None
 
-    return (await conn.execute(sql)).one_or_none()
+    return (await conn.execute(add_geometries(sql, t.c.geometry))).one_or_none()
 
 
 async def find_in_osmline(conn: SearchConnection, place: ntyp.PlaceRef,
-                          details: ntyp.LookupDetails) -> Optional[SaRow]:
+                          add_geometries: GeomFunc) -> Optional[SaRow]:
     """ Search for the given place in the osmline table and return the
         base information.
     """
@@ -76,8 +66,7 @@ async def find_in_osmline(conn: SearchConnection, place: ntyp.PlaceRef,
     sql = sa.select(t.c.place_id, t.c.osm_id, t.c.parent_place_id,
                     t.c.indexed_date, t.c.startnumber, t.c.endnumber,
                     t.c.step, t.c.address, t.c.postcode, t.c.country_code,
-                    t.c.linegeo.ST_Centroid().label('centroid'),
-                    _select_column_geometry(t.c.linegeo, details.geometry_output))
+                    t.c.linegeo.ST_Centroid().label('centroid'))
 
     if isinstance(place, ntyp.PlaceID):
         sql = sql.where(t.c.place_id == place.place_id)
@@ -92,14 +81,17 @@ async def find_in_osmline(conn: SearchConnection, place: ntyp.PlaceRef,
     else:
         return None
 
-    return (await conn.execute(sql)).one_or_none()
+    return (await conn.execute(add_geometries(sql, t.c.linegeo))).one_or_none()
 
 
 async def find_in_tiger(conn: SearchConnection, place: ntyp.PlaceRef,
-                        details: ntyp.LookupDetails) -> Optional[SaRow]:
+                        add_geometries: GeomFunc) -> Optional[SaRow]:
     """ Search for the given place in the table of Tiger addresses and return
         the base information. Only lookup by place ID is supported.
     """
+    if not isinstance(place, ntyp.PlaceID):
+        return None
+
     log().section("Find in TIGER table")
     t = conn.t.tiger
     parent = conn.t.placex
@@ -107,69 +99,86 @@ async def find_in_tiger(conn: SearchConnection, place: ntyp.PlaceRef,
                     parent.c.osm_type, parent.c.osm_id,
                     t.c.startnumber, t.c.endnumber, t.c.step,
                     t.c.postcode,
-                    t.c.linegeo.ST_Centroid().label('centroid'),
-                    _select_column_geometry(t.c.linegeo, details.geometry_output))
+                    t.c.linegeo.ST_Centroid().label('centroid'))\
+            .where(t.c.place_id == place.place_id)\
+            .join(parent, t.c.parent_place_id == parent.c.place_id, isouter=True)
 
-    if isinstance(place, ntyp.PlaceID):
-        sql = sql.where(t.c.place_id == place.place_id)\
-                 .join(parent, t.c.parent_place_id == parent.c.place_id, isouter=True)
-    else:
-        return None
-
-    return (await conn.execute(sql)).one_or_none()
+    return (await conn.execute(add_geometries(sql, t.c.linegeo))).one_or_none()
 
 
 async def find_in_postcode(conn: SearchConnection, place: ntyp.PlaceRef,
-                           details: ntyp.LookupDetails) -> Optional[SaRow]:
+                           add_geometries: GeomFunc) -> Optional[SaRow]:
     """ Search for the given place in the postcode table and return the
         base information. Only lookup by place ID is supported.
     """
+    if not isinstance(place, ntyp.PlaceID):
+        return None
+
     log().section("Find in postcode table")
     t = conn.t.postcode
     sql = sa.select(t.c.place_id, t.c.parent_place_id,
                     t.c.rank_search, t.c.rank_address,
                     t.c.indexed_date, t.c.postcode, t.c.country_code,
-                    t.c.geometry.label('centroid'),
-                    _select_column_geometry(t.c.geometry, details.geometry_output))
+                    t.c.geometry.label('centroid')) \
+            .where(t.c.place_id == place.place_id)
 
-    if isinstance(place, ntyp.PlaceID):
-        sql = sql.where(t.c.place_id == place.place_id)
-    else:
-        return None
+    return (await conn.execute(add_geometries(sql, t.c.geometry))).one_or_none()
 
-    return (await conn.execute(sql)).one_or_none()
 
+async def find_in_all_tables(conn: SearchConnection, place: ntyp.PlaceRef,
+                             add_geometries: GeomFunc
+                            ) -> Tuple[Optional[SaRow], RowFunc[nres.BaseResultT]]:
+    """ Search for the given place in all data tables
+        and return the base information.
+    """
+    row = await find_in_placex(conn, place, add_geometries)
+    log().var_dump('Result (placex)', row)
+    if row is not None:
+        return row, nres.create_from_placex_row
 
-async def get_place_by_id(conn: SearchConnection, place: ntyp.PlaceRef,
-                          details: ntyp.LookupDetails) -> Optional[nres.DetailedResult]:
+    row = await find_in_osmline(conn, place, add_geometries)
+    log().var_dump('Result (osmline)', row)
+    if row is not None:
+        return row, nres.create_from_osmline_row
+
+    row = await find_in_postcode(conn, place, add_geometries)
+    log().var_dump('Result (postcode)', row)
+    if row is not None:
+        return row, nres.create_from_postcode_row
+
+    row = await find_in_tiger(conn, place, add_geometries)
+    log().var_dump('Result (tiger)', row)
+    return row, nres.create_from_tiger_row
+
+
+async def get_detailed_place(conn: SearchConnection, place: ntyp.PlaceRef,
+                             details: ntyp.LookupDetails) -> Optional[nres.DetailedResult]:
     """ Retrieve a place with additional details from the database.
     """
-    log().function('get_place_by_id', place=place, details=details)
+    log().function('get_detailed_place', place=place, details=details)
 
     if details.geometry_output and details.geometry_output != ntyp.GeometryFormat.GEOJSON:
         raise ValueError("lookup only supports geojosn polygon output.")
 
-    row = await find_in_placex(conn, place, details)
-    log().var_dump('Result (placex)', row)
-    if row is not None:
-        result = nres.create_from_placex_row(row, nres.DetailedResult)
+    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'))
     else:
-        row = await find_in_osmline(conn, place, details)
-        log().var_dump('Result (osmline)', row)
-        if row is not None:
-            result = nres.create_from_osmline_row(row, nres.DetailedResult)
-        else:
-            row = await find_in_postcode(conn, place, details)
-            log().var_dump('Result (postcode)', row)
-            if row is not None:
-                result = nres.create_from_postcode_row(row, nres.DetailedResult)
-            else:
-                row = await find_in_tiger(conn, place, details)
-                log().var_dump('Result (tiger)', row)
-                if row is not None:
-                    result = nres.create_from_tiger_row(row, nres.DetailedResult)
-                else:
-                    return None
+        def _add_geometry(sql: SaSelect, column: SaColumn) -> SaSelect:
+            return sql.add_columns(sa.func.ST_GeometryType(column).label('geometry_type'))
+
+    row_func: RowFunc[nres.DetailedResult]
+    row, row_func = await find_in_all_tables(conn, place, _add_geometry)
+
+    if row is None:
+        return None
+
+    result = row_func(row, nres.DetailedResult)
+    assert result is not None
 
     # add missing details
     assert result is not None
@@ -183,3 +192,48 @@ async def get_place_by_id(conn: SearchConnection, place: ntyp.PlaceRef,
     await nres.add_result_details(conn, result, details)
 
     return result
+
+
+async def get_simple_place(conn: SearchConnection, place: ntyp.PlaceRef,
+                             details: ntyp.LookupDetails) -> Optional[nres.SearchResult]:
+    """ Retrieve a place as a simple search result from the database.
+    """
+    log().function('get_simple_place', place=place, details=details)
+
+    def _add_geometry(sql: SaSelect, col: SaColumn) -> SaSelect:
+        if not details.geometry_output:
+            return sql
+
+        out = []
+
+        if details.geometry_simplification > 0.0:
+            col = col.ST_SimplifyPreserveTopology(details.geometry_simplification)
+
+        if details.geometry_output & ntyp.GeometryFormat.GEOJSON:
+            out.append(col.ST_AsGeoJSON().label('geometry_geojson'))
+        if details.geometry_output & ntyp.GeometryFormat.TEXT:
+            out.append(col.ST_AsText().label('geometry_text'))
+        if details.geometry_output & ntyp.GeometryFormat.KML:
+            out.append(col.ST_AsKML().label('geometry_kml'))
+        if details.geometry_output & ntyp.GeometryFormat.SVG:
+            out.append(col.ST_AsSVG().label('geometry_svg'))
+
+        return sql.add_columns(*out)
+
+
+    row_func: RowFunc[nres.SearchResult]
+    row, row_func = await find_in_all_tables(conn, place, _add_geometry)
+
+    if row is None:
+        return None
+
+    result = row_func(row, nres.SearchResult)
+    assert result is not None
+
+    # add missing details
+    assert result is not None
+    result.bbox = getattr(row, 'bbox', None)
+
+    await nres.add_result_details(conn, result, details)
+
+    return result
index 098851ef1b8a39da145eba2b7ba5968f45e84f34..98b13380726e98361bc2571c85bd78928c1afc12 100644 (file)
@@ -173,6 +173,19 @@ class ReverseResults(List[ReverseResult]):
     """
 
 
+@dataclasses.dataclass
+class SearchResult(BaseResult):
+    """ A search result for forward geocoding.
+    """
+    bbox: Optional[Bbox] = None
+
+
+class SearchResults(List[SearchResult]):
+    """ Sequence of forward lookup results ordered by relevance.
+        May be empty when no result was found.
+    """
+
+
 def _filter_geometries(row: SaRow) -> Dict[str, str]:
     return {k[9:]: v for k, v in row._mapping.items() # pylint: disable=W0212
             if k.startswith('geometry_')}
index 27faa1746c3a105a946b7eae4194f23f6660cb41..273fe2f5bf97ec39fb8bfa8dd5861779a8e429cd 100644 (file)
@@ -10,7 +10,7 @@ Hard-coded information about tag catagories.
 These tables have been copied verbatim from the old PHP code. For future
 version a more flexible formatting is required.
 """
-from typing import Tuple, Optional, Mapping
+from typing import Tuple, Optional, Mapping, Union
 
 import nominatim.api as napi
 
@@ -41,7 +41,7 @@ def get_label_tag(category: Tuple[str, str], extratags: Optional[Mapping[str, st
     return label.lower().replace(' ', '_')
 
 
-def bbox_from_result(result: napi.ReverseResult) -> napi.Bbox:
+def bbox_from_result(result: Union[napi.ReverseResult, napi.SearchResult]) -> napi.Bbox:
     """ Compute a bounding box for the result. For ways and relations
         a given boundingbox is used. For all other object, a box is computed
         around the centroid according to dimensions dereived from the
index 47d2af4d49f2eaeeee87cae9ff1cef1ed604cc48..2e1caa991a91e0ca09eff6c458ed42b5ae8ff99b 100644 (file)
@@ -195,3 +195,36 @@ def _format_reverse_jsonv2(results: napi.ReverseResults,
                            options: Mapping[str, Any]) -> str:
     return format_json.format_base_json(results, options, True,
                                         class_label='category')
+
+
+@dispatch.format_func(napi.SearchResults, 'xml')
+def _format_search_xml(results: napi.SearchResults, options: Mapping[str, Any]) -> str:
+    return format_xml.format_base_xml(results,
+                                      options, False, 'searchresults',
+                                      {'querystring': 'TODO'})
+
+
+@dispatch.format_func(napi.SearchResults, 'geojson')
+def _format_search_geojson(results: napi.SearchResults,
+                            options: Mapping[str, Any]) -> str:
+    return format_json.format_base_geojson(results, options, False)
+
+
+@dispatch.format_func(napi.SearchResults, 'geocodejson')
+def _format_search_geocodejson(results: napi.SearchResults,
+                                options: Mapping[str, Any]) -> str:
+    return format_json.format_base_geocodejson(results, options, False)
+
+
+@dispatch.format_func(napi.SearchResults, 'json')
+def _format_search_json(results: napi.SearchResults,
+                         options: Mapping[str, Any]) -> str:
+    return format_json.format_base_json(results, options, False,
+                                        class_label='class')
+
+
+@dispatch.format_func(napi.SearchResults, 'jsonv2')
+def _format_search_jsonv2(results: napi.SearchResults,
+                           options: Mapping[str, Any]) -> str:
+    return format_json.format_base_json(results, options, False,
+                                        class_label='category')
index ef5f5280ee884fc82060990ce43123f813a39909..c82681e91f7078fc7446644fa800a08a97c28637 100644 (file)
@@ -7,12 +7,14 @@
 """
 Helper functions for output of results in json formats.
 """
-from typing import Mapping, Any, Optional, Tuple
+from typing import Mapping, Any, Optional, Tuple, Union
 
 import nominatim.api as napi
 import nominatim.api.v1.classtypes as cl
 from nominatim.utils.json_writer import JsonWriter
 
+#pylint: disable=too-many-branches
+
 def _write_osm_id(out: JsonWriter, osm_object: Optional[Tuple[str, int]]) -> None:
     if osm_object is not None:
         out.keyval_not_none('osm_type', cl.OSM_TYPE_NAME.get(osm_object[0], None))\
@@ -61,7 +63,7 @@ def _write_geocodejson_address(out: JsonWriter,
         out.keyval('country_code', country_code)
 
 
-def format_base_json(results: napi.ReverseResults, #pylint: disable=too-many-branches
+def format_base_json(results: Union[napi.ReverseResults, napi.SearchResults],
                      options: Mapping[str, Any], simple: bool,
                      class_label: str) -> str:
     """ Return the result list as a simple json string in custom Nominatim format.
@@ -141,7 +143,7 @@ def format_base_json(results: napi.ReverseResults, #pylint: disable=too-many-bra
     return out()
 
 
-def format_base_geojson(results: napi.ReverseResults,
+def format_base_geojson(results: Union[napi.ReverseResults, napi.SearchResults],
                         options: Mapping[str, Any],
                         simple: bool) -> str:
     """ Return the result list as a geojson string.
@@ -210,7 +212,7 @@ def format_base_geojson(results: napi.ReverseResults,
     return out()
 
 
-def format_base_geocodejson(results: napi.ReverseResults,
+def format_base_geocodejson(results: Union[napi.ReverseResults, napi.SearchResults],
                             options: Mapping[str, Any], simple: bool) -> str:
     """ Return the result list as a geocodejson string.
     """
@@ -249,7 +251,7 @@ def format_base_geocodejson(results: napi.ReverseResults,
         out.keyval('osm_key', result.category[0])\
            .keyval('osm_value', result.category[1])\
            .keyval('type', GEOCODEJSON_RANKS[max(3, min(28, result.rank_address))])\
-           .keyval_not_none('accuracy', result.distance, transform=int)\
+           .keyval_not_none('accuracy', getattr(result, 'distance', None), transform=int)\
            .keyval('label', ', '.join(label_parts))\
            .keyval_not_none('name', result.names, transform=locales.display_name)\
 
index 3fe3b7fe7771a428b57062c97a59e9c8f797c79f..1fd0675a36d04f88026ebebdc6730a720605a37d 100644 (file)
@@ -7,13 +7,15 @@
 """
 Helper functions for output of results in XML format.
 """
-from typing import Mapping, Any, Optional
+from typing import Mapping, Any, Optional, Union
 import datetime as dt
 import xml.etree.ElementTree as ET
 
 import nominatim.api as napi
 import nominatim.api.v1.classtypes as cl
 
+#pylint: disable=too-many-branches
+
 def _write_xml_address(root: ET.Element, address: napi.AddressLines,
                        country_code: Optional[str]) -> None:
     parts = {}
@@ -34,7 +36,7 @@ def _write_xml_address(root: ET.Element, address: napi.AddressLines,
         ET.SubElement(root, 'country_code').text = country_code
 
 
-def _create_base_entry(result: napi.ReverseResult, #pylint: disable=too-many-branches
+def _create_base_entry(result: Union[napi.ReverseResult, napi.SearchResult],
                        root: ET.Element, simple: bool,
                        locales: napi.Locales) -> ET.Element:
     if result.address_rows:
@@ -86,7 +88,7 @@ def _create_base_entry(result: napi.ReverseResult, #pylint: disable=too-many-bra
     return place
 
 
-def format_base_xml(results: napi.ReverseResults,
+def format_base_xml(results: Union[napi.ReverseResults, napi.SearchResults],
                     options: Mapping[str, Any],
                     simple: bool, xml_root_tag: str,
                     xml_extra_info: Mapping[str, str]) -> str:
index a87b682554fe202b025c69648d9a45158a217cdd..68cf58c285b37858dc90828cde711ac79824743a 100644 (file)
@@ -226,6 +226,33 @@ class ASGIAdaptor(abc.ABC):
         return fmt
 
 
+    def parse_geometry_details(self, fmt: str) -> napi.LookupDetails:
+        """ Create details strucutre from the supplied geometry parameters.
+        """
+        details = napi.LookupDetails(address_details=True,
+                                     geometry_simplification=
+                                       self.get_float('polygon_threshold', 0.0))
+        numgeoms = 0
+        if self.get_bool('polygon_geojson', False):
+            details.geometry_output |= napi.GeometryFormat.GEOJSON
+            numgeoms += 1
+        if fmt not in ('geojson', 'geocodejson'):
+            if self.get_bool('polygon_text', False):
+                details.geometry_output |= napi.GeometryFormat.TEXT
+                numgeoms += 1
+            if self.get_bool('polygon_kml', False):
+                details.geometry_output |= napi.GeometryFormat.KML
+                numgeoms += 1
+            if self.get_bool('polygon_svg', False):
+                details.geometry_output |= napi.GeometryFormat.SVG
+                numgeoms += 1
+
+        if numgeoms > self.config().get_int('POLYGON_OUTPUT_MAX_TYPES'):
+            self.raise_error('Too many polgyon output options selected.')
+
+        return details
+
+
 async def status_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) -> Any:
     """ Server glue for /status endpoint. See API docs for details.
     """
@@ -268,7 +295,7 @@ async def details_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) ->
 
     locales = napi.Locales.from_accept_languages(params.get_accepted_languages())
 
-    result = await api.lookup(place, details)
+    result = await api.details(place, details)
 
     if debug:
         return params.build_response(loglib.get_and_disable())
@@ -291,28 +318,10 @@ async def reverse_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) ->
     debug = params.setup_debugging()
     coord = napi.Point(params.get_float('lon'), params.get_float('lat'))
     locales = napi.Locales.from_accept_languages(params.get_accepted_languages())
+    details = params.parse_geometry_details(fmt)
 
     zoom = max(0, min(18, params.get_int('zoom', 18)))
 
-    details = napi.LookupDetails(address_details=True,
-                                 geometry_simplification=params.get_float('polygon_threshold', 0.0))
-    numgeoms = 0
-    if params.get_bool('polygon_geojson', False):
-        details.geometry_output |= napi.GeometryFormat.GEOJSON
-        numgeoms += 1
-    if fmt not in ('geojson', 'geocodejson'):
-        if params.get_bool('polygon_text', False):
-            details.geometry_output |= napi.GeometryFormat.TEXT
-            numgeoms += 1
-        if params.get_bool('polygon_kml', False):
-            details.geometry_output |= napi.GeometryFormat.KML
-            numgeoms += 1
-        if params.get_bool('polygon_svg', False):
-            details.geometry_output |= napi.GeometryFormat.SVG
-            numgeoms += 1
-
-    if numgeoms > params.config().get_int('POLYGON_OUTPUT_MAX_TYPES'):
-        params.raise_error('Too many polgyon output options selected.')
 
     result = await api.reverse(coord, REVERSE_MAX_RANKS[zoom],
                                params.get_layers() or
@@ -326,9 +335,6 @@ async def reverse_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) ->
                    'extratags': params.get_bool('extratags', False),
                    'namedetails': params.get_bool('namedetails', False),
                    'addressdetails': params.get_bool('addressdetails', True)}
-    if fmt == 'xml':
-        fmt_options['xml_roottag'] = 'reversegeocode'
-        fmt_options['xml_extra_info'] = {'querystring': 'TODO'}
 
     output = formatting.format_result(napi.ReverseResults([result] if result else []),
                                       fmt, fmt_options)
@@ -336,6 +342,37 @@ async def reverse_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) ->
     return params.build_response(output)
 
 
+async def lookup_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) -> Any:
+    """ Server glue for /lookup endpoint. See API docs for details.
+    """
+    fmt = params.parse_format(napi.SearchResults, 'xml')
+    debug = params.setup_debugging()
+    locales = napi.Locales.from_accept_languages(params.get_accepted_languages())
+    details = params.parse_geometry_details(fmt)
+
+    places = []
+    for oid in (params.get('osm_ids') or '').split(','):
+        oid = oid.strip()
+        if len(oid) > 1 and oid[0] in 'RNWrnw' and oid[1:].isdigit():
+            places.append(napi.OsmID(oid[0], int(oid[1:])))
+
+    if places:
+        results = await api.lookup(places, details)
+    else:
+        results = napi.SearchResults()
+
+    if debug:
+        return params.build_response(loglib.get_and_disable())
+
+    fmt_options = {'locales': locales,
+                   'extratags': params.get_bool('extratags', False),
+                   'namedetails': params.get_bool('namedetails', False),
+                   'addressdetails': params.get_bool('addressdetails', True)}
+
+    output = formatting.format_result(results, fmt, fmt_options)
+
+    return params.build_response(output)
+
 EndpointFunc = Callable[[napi.NominatimAPIAsync, ASGIAdaptor], Any]
 
 REVERSE_MAX_RANKS = [2, 2, 2,   # 0-2   Continent/Sea
@@ -357,5 +394,6 @@ REVERSE_MAX_RANKS = [2, 2, 2,   # 0-2   Continent/Sea
 ROUTES = [
     ('status', status_endpoint),
     ('details', details_endpoint),
-    ('reverse', reverse_endpoint)
+    ('reverse', reverse_endpoint),
+    ('lookup', lookup_endpoint)
 ]
index 41256b792690b76688628e46a253a5410fdcd27f..58edbea4b8776d7b5705ea1e123344a57d8b80bf 100644 (file)
@@ -214,19 +214,31 @@ class APILookup:
 
 
     def run(self, args: NominatimArgs) -> int:
-        params: Dict[str, object] = dict(osm_ids=','.join(args.ids), format=args.format)
+        api = napi.NominatimAPI(args.project_dir)
 
-        for param, _ in EXTRADATA_PARAMS:
-            if getattr(args, param):
-                params[param] = '1'
-        if args.lang:
-            params['accept-language'] = args.lang
-        if args.polygon_output:
-            params['polygon_' + args.polygon_output] = '1'
-        if args.polygon_threshold:
-            params['polygon_threshold'] = args.polygon_threshold
+        details = napi.LookupDetails(address_details=True, # needed for display name
+                                     geometry_output=args.get_geometry_output(),
+                                     geometry_simplification=args.polygon_threshold or 0.0)
+
+        places = [napi.OsmID(o[0], int(o[1:])) for o in args.ids]
+
+        results = api.lookup(places, details)
 
-        return _run_api('lookup', args, params)
+        output = api_output.format_result(
+                    results,
+                    args.format,
+                    {'locales': args.get_locales(api.config.DEFAULT_LANGUAGE),
+                     'extratags': args.extratags,
+                     'namedetails': args.namedetails,
+                     'addressdetails': args.addressdetails})
+        if args.format != 'xml':
+            # reformat the result, so it is pretty-printed
+            json.dump(json.loads(output), sys.stdout, indent=4, ensure_ascii=False)
+        else:
+            sys.stdout.write(output)
+        sys.stdout.write('\n')
+
+        return 0
 
 
 class APIDetails:
@@ -292,7 +304,7 @@ class APIDetails:
         if args.polygon_geojson:
             details.geometry_output = napi.GeometryFormat.GEOJSON
 
-        result = api.lookup(place, details)
+        result = api.details(place, details)
 
         if result:
             output = api_output.format_result(
index 0b939f593b4649803985ac42cab6fd9c5f5c83e8..3c67d652e42fa59bc01a658b4b820b784c1891c1 100644 (file)
@@ -29,14 +29,14 @@ Feature: Import of address interpolations
           | N2  | place | house  | 8       |
         And the places
           | osm | class | type   | addr+interpolation | geometry |
-          | W1  | place | houses | even    | 1,2 |
+          | W1  | place | houses | even               | 2,1      |
         And the ways
           | id | nodes |
           | 1  | 2,1 |
         When importing
         Then W1 expands to interpolation
           | start | end | geometry |
-          | 4     | 6   | 8,9 |
+          | 4     | 6   | 9,8      |
 
     Scenario: Simple odd two point interpolation
         Given the grid with origin 1,1
@@ -341,7 +341,7 @@ Feature: Import of address interpolations
         Then W1 expands to interpolation
           | start | end | geometry |
           | 4     | 4   | 144.963016 -37.762946 |
-          | 8     | 8   | 144.963144 -37.7622237 |
+          | 8     | 8   | 144.96314407 -37.762223692 |
 
     Scenario: Place with missing address information
         Given the grid
@@ -456,3 +456,69 @@ Feature: Import of address interpolations
           | foo   |
           | x     |
           | 12-2  |
+
+
+    Scenario: Interpolation line where points have been moved (Github #3022)
+        Given the 0.00001 grid
+         | 1 | | | | | | | | 2 | 3 | 9 | | | | | | | | 4 |
+        Given the places
+          | osm | class | type   | housenr | geometry |
+          | N1  | place | house  | 2       | 1 |
+          | N2  | place | house  | 18      | 3 |
+          | N3  | place | house  | 24      | 9 |
+          | N4  | place | house  | 42      | 4 |
+        And the places
+          | osm | class | type   | addr+interpolation | geometry |
+          | W1  | place | houses | even               | 1,2,3,4  |
+        And the ways
+          | id | nodes   |
+          | 1  | 1,2,3,4 |
+        When importing
+        Then W1 expands to interpolation
+          | start | end |
+          | 4     | 16  |
+          | 20    | 22  |
+          | 26    | 40  |
+
+
+    Scenario: Interpolation line with duplicated points
+        Given the grid
+          | 7 | 10 | 8 | 11 | 9 |
+        Given the places
+          | osm | class | type   | housenr | geometry |
+          | N1  | place | house  | 2       | 7 |
+          | N2  | place | house  | 6       | 8 |
+          | N3  | place | house  | 10      | 8 |
+          | N4  | place | house  | 14      | 9 |
+        And the places
+          | osm | class | type   | addr+interpolation | geometry |
+          | W1  | place | houses | even               | 7,8,8,9  |
+        And the ways
+          | id | nodes   |
+          | 1  | 1,2,3,4 |
+        When importing
+        Then W1 expands to interpolation
+          | start | end | geometry |
+          | 4     | 4   | 10       |
+          | 12    | 12  | 11       |
+
+
+    Scenario: Interpolaton line with broken way geometry (Github #2986)
+        Given the grid
+          | 1 | 8 | 10 | 11 | 9 | 2 | 3 | 4 |
+        Given the places
+          | osm | class | type   | housenr |
+          | N1  | place | house  | 2       |
+          | N2  | place | house  | 8       |
+          | N3  | place | house  | 12      |
+          | N4  | place | house  | 14      |
+        And the places
+          | osm | class | type   | addr+interpolation | geometry |
+          | W1  | place | houses | even               | 8,9      |
+        And the ways
+          | id | nodes       |
+          | 1  | 1,8,9,2,3,4 |
+        When importing
+        Then W1 expands to interpolation
+          | start | end | geometry |
+          | 4     | 6   | 10,11    |
diff --git a/test/python/api/test_api_details.py b/test/python/api/test_api_details.py
new file mode 100644 (file)
index 0000000..625c4e7
--- /dev/null
@@ -0,0 +1,584 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# This file is part of Nominatim. (https://nominatim.org)
+#
+# Copyright (C) 2023 by the Nominatim developer community.
+# For a full list of authors see the git log.
+"""
+Tests for details API call.
+"""
+import datetime as dt
+
+import pytest
+
+import nominatim.api as napi
+
+@pytest.mark.parametrize('idobj', (napi.PlaceID(332), napi.OsmID('W', 4),
+                                   napi.OsmID('W', 4, 'highway')))
+def test_lookup_in_placex(apiobj, 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',
+                     name={'name': 'Road'}, address={'city': 'Barrow'},
+                     extratags={'surface': 'paved'},
+                     parent_place_id=34, linked_place_id=55,
+                     admin_level=15, country_code='gb',
+                     housenumber='4',
+                     postcode='34425', wikipedia='en:Faa',
+                     rank_search=27, rank_address=26,
+                     importance=0.01,
+                     centroid=(23, 34),
+                     indexed_date=import_date,
+                     geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)')
+
+    result = apiobj.api.details(idobj, napi.LookupDetails())
+
+    assert result is not None
+
+    assert result.source_table.name == 'PLACEX'
+    assert result.category == ('highway', 'residential')
+    assert result.centroid == (pytest.approx(23.0), pytest.approx(34.0))
+
+    assert result.place_id == 332
+    assert result.parent_place_id == 34
+    assert result.linked_place_id == 55
+    assert result.osm_object == ('W', 4)
+    assert result.admin_level == 15
+
+    assert result.names == {'name': 'Road'}
+    assert result.address == {'city': 'Barrow'}
+    assert result.extratags == {'surface': 'paved'}
+
+    assert result.housenumber == '4'
+    assert result.postcode == '34425'
+    assert result.wikipedia == 'en:Faa'
+
+    assert result.rank_search == 27
+    assert result.rank_address == 26
+    assert result.importance == pytest.approx(0.01)
+
+    assert result.country_code == 'gb'
+    assert result.indexed_date == import_date.replace(tzinfo=dt.timezone.utc)
+
+    assert result.address_rows is None
+    assert result.linked_rows is None
+    assert result.parented_rows is None
+    assert result.name_keywords is None
+    assert result.address_keywords is None
+
+    assert result.geometry == {'type': 'ST_LineString'}
+
+
+def test_lookup_in_placex_minimal_info(apiobj):
+    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',
+                     admin_level=15,
+                     rank_search=27, rank_address=26,
+                     centroid=(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), napi.LookupDetails())
+
+    assert result is not None
+
+    assert result.source_table.name == 'PLACEX'
+    assert result.category == ('highway', 'residential')
+    assert result.centroid == (pytest.approx(23.0), pytest.approx(34.0))
+
+    assert result.place_id == 332
+    assert result.parent_place_id is None
+    assert result.linked_place_id is None
+    assert result.osm_object == ('W', 4)
+    assert result.admin_level == 15
+
+    assert result.names is None
+    assert result.address is None
+    assert result.extratags is None
+
+    assert result.housenumber is None
+    assert result.postcode is None
+    assert result.wikipedia is None
+
+    assert result.rank_search == 27
+    assert result.rank_address == 26
+    assert result.importance is None
+
+    assert result.country_code is None
+    assert result.indexed_date == import_date.replace(tzinfo=dt.timezone.utc)
+
+    assert result.address_rows is None
+    assert result.linked_rows is None
+    assert result.parented_rows is None
+    assert result.name_keywords is None
+    assert result.address_keywords is None
+
+    assert result.geometry == {'type': 'ST_LineString'}
+
+
+def test_lookup_in_placex_with_geometry(apiobj):
+    apiobj.add_placex(place_id=332,
+                      geometry='LINESTRING(23 34, 23.1 34)')
+
+    result = apiobj.api.details(napi.PlaceID(332),
+                               napi.LookupDetails(geometry_output=napi.GeometryFormat.GEOJSON))
+
+    assert result.geometry == {'geojson': '{"type":"LineString","coordinates":[[23,34],[23.1,34]]}'}
+
+
+def test_lookup_placex_with_address_details(apiobj):
+    apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
+                     class_='highway', type='residential',  name='Street',
+                     country_code='pl',
+                     rank_search=27, rank_address=26)
+    apiobj.add_address_placex(332, fromarea=False, isaddress=False,
+                              distance=0.0034,
+                              place_id=1000, osm_type='N', osm_id=3333,
+                              class_='place', type='suburb', name='Smallplace',
+                              country_code='pl', admin_level=13,
+                              rank_search=24, rank_address=23)
+    apiobj.add_address_placex(332, fromarea=True, isaddress=True,
+                              place_id=1001, osm_type='N', osm_id=3334,
+                              class_='place', type='city', name='Bigplace',
+                              country_code='pl',
+                              rank_search=17, rank_address=16)
+
+    result = apiobj.api.details(napi.PlaceID(332),
+                               napi.LookupDetails(address_details=True))
+
+    assert result.address_rows == [
+               napi.AddressLine(place_id=332, osm_object=('W', 4),
+                                category=('highway', 'residential'),
+                                names={'name': 'Street'}, extratags={},
+                                admin_level=15, fromarea=True, isaddress=True,
+                                rank_address=26, distance=0.0),
+               napi.AddressLine(place_id=1000, osm_object=('N', 3333),
+                                category=('place', 'suburb'),
+                                names={'name': 'Smallplace'}, extratags={},
+                                admin_level=13, fromarea=False, isaddress=True,
+                                rank_address=23, distance=0.0034),
+               napi.AddressLine(place_id=1001, osm_object=('N', 3334),
+                                category=('place', 'city'),
+                                names={'name': 'Bigplace'}, extratags={},
+                                admin_level=15, fromarea=True, isaddress=True,
+                                rank_address=16, distance=0.0),
+               napi.AddressLine(place_id=None, osm_object=None,
+                                category=('place', 'country_code'),
+                                names={'ref': 'pl'}, extratags={},
+                                admin_level=None, fromarea=True, isaddress=False,
+                                rank_address=4, distance=0.0)
+           ]
+
+
+def test_lookup_place_with_linked_places_none_existing(apiobj):
+    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),
+                               napi.LookupDetails(linked_places=True))
+
+    assert result.linked_rows == []
+
+
+def test_lookup_place_with_linked_places_existing(apiobj):
+    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=1001, osm_type='W', osm_id=5,
+                     class_='highway', type='residential',  name='Street',
+                     country_code='pl', linked_place_id=332,
+                     rank_search=27, rank_address=26)
+    apiobj.add_placex(place_id=1002, osm_type='W', osm_id=6,
+                     class_='highway', type='residential',  name='Street',
+                     country_code='pl', linked_place_id=332,
+                     rank_search=27, rank_address=26)
+
+    result = apiobj.api.details(napi.PlaceID(332),
+                               napi.LookupDetails(linked_places=True))
+
+    assert result.linked_rows == [
+               napi.AddressLine(place_id=1001, osm_object=('W', 5),
+                                category=('highway', 'residential'),
+                                names={'name': 'Street'}, extratags={},
+                                admin_level=15, fromarea=False, isaddress=True,
+                                rank_address=26, distance=0.0),
+               napi.AddressLine(place_id=1002, osm_object=('W', 6),
+                                category=('highway', 'residential'),
+                                names={'name': 'Street'}, extratags={},
+                                admin_level=15, fromarea=False, isaddress=True,
+                                rank_address=26, distance=0.0),
+    ]
+
+
+def test_lookup_place_with_parented_places_not_existing(apiobj):
+    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),
+                               napi.LookupDetails(parented_places=True))
+
+    assert result.parented_rows == []
+
+
+def test_lookup_place_with_parented_places_existing(apiobj):
+    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=1001, osm_type='N', osm_id=5,
+                     class_='place', type='house', housenumber='23',
+                     country_code='pl', parent_place_id=332,
+                     rank_search=30, rank_address=30)
+    apiobj.add_placex(place_id=1002, osm_type='W', osm_id=6,
+                     class_='highway', type='residential',  name='Street',
+                     country_code='pl', parent_place_id=332,
+                     rank_search=27, rank_address=26)
+
+    result = apiobj.api.details(napi.PlaceID(332),
+                               napi.LookupDetails(parented_places=True))
+
+    assert result.parented_rows == [
+               napi.AddressLine(place_id=1001, osm_object=('N', 5),
+                                category=('place', 'house'),
+                                names={'housenumber': '23'}, extratags={},
+                                admin_level=15, fromarea=False, isaddress=True,
+                                rank_address=30, distance=0.0),
+    ]
+
+
+@pytest.mark.parametrize('idobj', (napi.PlaceID(4924), napi.OsmID('W', 9928)))
+def test_lookup_in_osmline(apiobj, 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,
+                       startnumber=1, endnumber=4, step=1,
+                       country_code='gb', postcode='34425',
+                       address={'city': 'Big'},
+                       indexed_date=import_date,
+                       geometry='LINESTRING(23 34, 23 35)')
+
+    result = apiobj.api.details(idobj, napi.LookupDetails())
+
+    assert result is not None
+
+    assert result.source_table.name == 'OSMLINE'
+    assert result.category == ('place', 'houses')
+    assert result.centroid == (pytest.approx(23.0), pytest.approx(34.5))
+
+    assert result.place_id == 4924
+    assert result.parent_place_id == 12
+    assert result.linked_place_id is None
+    assert result.osm_object == ('W', 9928)
+    assert result.admin_level == 15
+
+    assert result.names is None
+    assert result.address == {'city': 'Big'}
+    assert result.extratags == {'startnumber': '1', 'endnumber': '4', 'step': '1'}
+
+    assert result.housenumber is None
+    assert result.postcode == '34425'
+    assert result.wikipedia is None
+
+    assert result.rank_search == 30
+    assert result.rank_address == 30
+    assert result.importance is None
+
+    assert result.country_code == 'gb'
+    assert result.indexed_date == import_date.replace(tzinfo=dt.timezone.utc)
+
+    assert result.address_rows is None
+    assert result.linked_rows is None
+    assert result.parented_rows is None
+    assert result.name_keywords is None
+    assert result.address_keywords is None
+
+    assert result.geometry == {'type': 'ST_LineString'}
+
+
+def test_lookup_in_osmline_split_interpolation(apiobj):
+    apiobj.add_osmline(place_id=1000, osm_id=9,
+                       startnumber=2, endnumber=4, step=1)
+    apiobj.add_osmline(place_id=1001, osm_id=9,
+                       startnumber=6, endnumber=9, step=1)
+    apiobj.add_osmline(place_id=1002, osm_id=9,
+                       startnumber=11, endnumber=20, step=1)
+
+    for i in range(1, 6):
+        result = apiobj.api.details(napi.OsmID('W', 9, str(i)), napi.LookupDetails())
+        assert result.place_id == 1000
+    for i in range(7, 11):
+        result = apiobj.api.details(napi.OsmID('W', 9, str(i)), napi.LookupDetails())
+        assert result.place_id == 1001
+    for i in range(12, 22):
+        result = apiobj.api.details(napi.OsmID('W', 9, str(i)), napi.LookupDetails())
+        assert result.place_id == 1002
+
+
+def test_lookup_osmline_with_address_details(apiobj):
+    apiobj.add_osmline(place_id=9000, osm_id=9,
+                       startnumber=2, endnumber=4, step=1,
+                       parent_place_id=332)
+    apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
+                     class_='highway', type='residential',  name='Street',
+                     country_code='pl',
+                     rank_search=27, rank_address=26)
+    apiobj.add_address_placex(332, fromarea=False, isaddress=False,
+                              distance=0.0034,
+                              place_id=1000, osm_type='N', osm_id=3333,
+                              class_='place', type='suburb', name='Smallplace',
+                              country_code='pl', admin_level=13,
+                              rank_search=24, rank_address=23)
+    apiobj.add_address_placex(332, fromarea=True, isaddress=True,
+                              place_id=1001, osm_type='N', osm_id=3334,
+                              class_='place', type='city', name='Bigplace',
+                              country_code='pl',
+                              rank_search=17, rank_address=16)
+
+    result = apiobj.api.details(napi.PlaceID(9000),
+                               napi.LookupDetails(address_details=True))
+
+    assert result.address_rows == [
+               napi.AddressLine(place_id=None, osm_object=None,
+                                category=('place', 'house_number'),
+                                names={'ref': '2'}, extratags={},
+                                admin_level=None, fromarea=True, isaddress=True,
+                                rank_address=28, distance=0.0),
+               napi.AddressLine(place_id=332, osm_object=('W', 4),
+                                category=('highway', 'residential'),
+                                names={'name': 'Street'}, extratags={},
+                                admin_level=15, fromarea=True, isaddress=True,
+                                rank_address=26, distance=0.0),
+               napi.AddressLine(place_id=1000, osm_object=('N', 3333),
+                                category=('place', 'suburb'),
+                                names={'name': 'Smallplace'}, extratags={},
+                                admin_level=13, fromarea=False, isaddress=True,
+                                rank_address=23, distance=0.0034),
+               napi.AddressLine(place_id=1001, osm_object=('N', 3334),
+                                category=('place', 'city'),
+                                names={'name': 'Bigplace'}, extratags={},
+                                admin_level=15, fromarea=True, isaddress=True,
+                                rank_address=16, distance=0.0),
+               napi.AddressLine(place_id=None, osm_object=None,
+                                category=('place', 'country_code'),
+                                names={'ref': 'pl'}, extratags={},
+                                admin_level=None, fromarea=True, isaddress=False,
+                                rank_address=4, distance=0.0)
+           ]
+
+
+def test_lookup_in_tiger(apiobj):
+    apiobj.add_tiger(place_id=4924,
+                     parent_place_id=12,
+                     startnumber=1, endnumber=4, step=1,
+                     postcode='34425',
+                     geometry='LINESTRING(23 34, 23 35)')
+    apiobj.add_placex(place_id=12,
+                      category=('highway', 'residential'),
+                      osm_type='W', osm_id=6601223,
+                      geometry='LINESTRING(23 34, 23 35)')
+
+    result = apiobj.api.details(napi.PlaceID(4924), napi.LookupDetails())
+
+    assert result is not None
+
+    assert result.source_table.name == 'TIGER'
+    assert result.category == ('place', 'houses')
+    assert result.centroid == (pytest.approx(23.0), pytest.approx(34.5))
+
+    assert result.place_id == 4924
+    assert result.parent_place_id == 12
+    assert result.linked_place_id is None
+    assert result.osm_object == ('W', 6601223)
+    assert result.admin_level == 15
+
+    assert result.names is None
+    assert result.address is None
+    assert result.extratags == {'startnumber': '1', 'endnumber': '4', 'step': '1'}
+
+    assert result.housenumber is None
+    assert result.postcode == '34425'
+    assert result.wikipedia is None
+
+    assert result.rank_search == 30
+    assert result.rank_address == 30
+    assert result.importance is None
+
+    assert result.country_code == 'us'
+    assert result.indexed_date is None
+
+    assert result.address_rows is None
+    assert result.linked_rows is None
+    assert result.parented_rows is None
+    assert result.name_keywords is None
+    assert result.address_keywords is None
+
+    assert result.geometry == {'type': 'ST_LineString'}
+
+
+def test_lookup_tiger_with_address_details(apiobj):
+    apiobj.add_tiger(place_id=9000,
+                     startnumber=2, endnumber=4, step=1,
+                     parent_place_id=332)
+    apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
+                     class_='highway', type='residential',  name='Street',
+                     country_code='us',
+                     rank_search=27, rank_address=26)
+    apiobj.add_address_placex(332, fromarea=False, isaddress=False,
+                              distance=0.0034,
+                              place_id=1000, osm_type='N', osm_id=3333,
+                              class_='place', type='suburb', name='Smallplace',
+                              country_code='us', admin_level=13,
+                              rank_search=24, rank_address=23)
+    apiobj.add_address_placex(332, fromarea=True, isaddress=True,
+                              place_id=1001, osm_type='N', osm_id=3334,
+                              class_='place', type='city', name='Bigplace',
+                              country_code='us',
+                              rank_search=17, rank_address=16)
+
+    result = apiobj.api.details(napi.PlaceID(9000),
+                               napi.LookupDetails(address_details=True))
+
+    assert result.address_rows == [
+               napi.AddressLine(place_id=None, osm_object=None,
+                                category=('place', 'house_number'),
+                                names={'ref': '2'}, extratags={},
+                                admin_level=None, fromarea=True, isaddress=True,
+                                rank_address=28, distance=0.0),
+               napi.AddressLine(place_id=332, osm_object=('W', 4),
+                                category=('highway', 'residential'),
+                                names={'name': 'Street'}, extratags={},
+                                admin_level=15, fromarea=True, isaddress=True,
+                                rank_address=26, distance=0.0),
+               napi.AddressLine(place_id=1000, osm_object=('N', 3333),
+                                category=('place', 'suburb'),
+                                names={'name': 'Smallplace'}, extratags={},
+                                admin_level=13, fromarea=False, isaddress=True,
+                                rank_address=23, distance=0.0034),
+               napi.AddressLine(place_id=1001, osm_object=('N', 3334),
+                                category=('place', 'city'),
+                                names={'name': 'Bigplace'}, extratags={},
+                                admin_level=15, fromarea=True, isaddress=True,
+                                rank_address=16, distance=0.0),
+               napi.AddressLine(place_id=None, osm_object=None,
+                                category=('place', 'country_code'),
+                                names={'ref': 'us'}, extratags={},
+                                admin_level=None, fromarea=True, isaddress=False,
+                                rank_address=4, distance=0.0)
+           ]
+
+
+def test_lookup_in_postcode(apiobj):
+    import_date = dt.datetime(2022, 12, 7, 14, 14, 46, 0)
+    apiobj.add_postcode(place_id=554,
+                        parent_place_id=152,
+                        postcode='34 425',
+                        country_code='gb',
+                        rank_search=20, rank_address=22,
+                        indexed_date=import_date,
+                        geometry='POINT(-9.45 5.6)')
+
+    result = apiobj.api.details(napi.PlaceID(554), napi.LookupDetails())
+
+    assert result is not None
+
+    assert result.source_table.name == 'POSTCODE'
+    assert result.category == ('place', 'postcode')
+    assert result.centroid == (pytest.approx(-9.45), pytest.approx(5.6))
+
+    assert result.place_id == 554
+    assert result.parent_place_id == 152
+    assert result.linked_place_id is None
+    assert result.osm_object is None
+    assert result.admin_level == 15
+
+    assert result.names == {'ref': '34 425'}
+    assert result.address is None
+    assert result.extratags is None
+
+    assert result.housenumber is None
+    assert result.postcode is None
+    assert result.wikipedia is None
+
+    assert result.rank_search == 20
+    assert result.rank_address == 22
+    assert result.importance is None
+
+    assert result.country_code == 'gb'
+    assert result.indexed_date == import_date.replace(tzinfo=dt.timezone.utc)
+
+    assert result.address_rows is None
+    assert result.linked_rows is None
+    assert result.parented_rows is None
+    assert result.name_keywords is None
+    assert result.address_keywords is None
+
+    assert result.geometry == {'type': 'ST_Point'}
+
+
+def test_lookup_postcode_with_address_details(apiobj):
+    apiobj.add_postcode(place_id=9000,
+                        parent_place_id=332,
+                        postcode='34 425',
+                        country_code='gb',
+                        rank_search=25, rank_address=25)
+    apiobj.add_placex(place_id=332, osm_type='N', osm_id=3333,
+                      class_='place', type='suburb',  name='Smallplace',
+                      country_code='gb', admin_level=13,
+                      rank_search=24, rank_address=23)
+    apiobj.add_address_placex(332, fromarea=True, isaddress=True,
+                              place_id=1001, osm_type='N', osm_id=3334,
+                              class_='place', type='city', name='Bigplace',
+                              country_code='gb',
+                              rank_search=17, rank_address=16)
+
+    result = apiobj.api.details(napi.PlaceID(9000),
+                               napi.LookupDetails(address_details=True))
+
+    assert result.address_rows == [
+               napi.AddressLine(place_id=332, osm_object=('N', 3333),
+                                category=('place', 'suburb'),
+                                names={'name': 'Smallplace'}, extratags={},
+                                admin_level=13, fromarea=True, isaddress=True,
+                                rank_address=23, distance=0.0),
+               napi.AddressLine(place_id=1001, osm_object=('N', 3334),
+                                category=('place', 'city'),
+                                names={'name': 'Bigplace'}, extratags={},
+                                admin_level=15, fromarea=True, isaddress=True,
+                                rank_address=16, distance=0.0),
+               napi.AddressLine(place_id=None, osm_object=None,
+                                category=('place', 'postcode'),
+                                names={'ref': '34 425'}, extratags={},
+                                admin_level=None, fromarea=False, isaddress=True,
+                                rank_address=5, distance=0.0),
+               napi.AddressLine(place_id=None, osm_object=None,
+                                category=('place', 'country_code'),
+                                names={'ref': 'gb'}, extratags={},
+                                admin_level=None, fromarea=True, isaddress=False,
+                                rank_address=4, distance=0.0)
+           ]
+
+@pytest.mark.parametrize('objid', [napi.PlaceID(1736),
+                                   napi.OsmID('W', 55),
+                                   napi.OsmID('N', 55, 'amenity')])
+def test_lookup_missing_object(apiobj, objid):
+    apiobj.add_placex(place_id=1, osm_type='N', osm_id=55,
+                      class_='place', type='suburb')
+
+    assert apiobj.api.details(objid, napi.LookupDetails()) is None
+
+
+@pytest.mark.parametrize('gtype', (napi.GeometryFormat.KML,
+                                    napi.GeometryFormat.SVG,
+                                    napi.GeometryFormat.TEXT))
+def test_lookup_unsupported_geometry(apiobj, gtype):
+    apiobj.add_placex(place_id=332)
+
+    with pytest.raises(ValueError):
+        apiobj.api.details(napi.PlaceID(332),
+                          napi.LookupDetails(geometry_output=gtype))
index 6939ddb9ba28c19ef59656ea9652511fb9272f11..6aafa29eed5a6fd41e00f8d4ca2fbf5541f07e02 100644 (file)
@@ -7,16 +7,22 @@
 """
 Tests for lookup API call.
 """
-import datetime as dt
-
 import pytest
 
 import nominatim.api as napi
 
+def test_lookup_empty_list(apiobj):
+    assert apiobj.api.lookup([]) == []
+
+
+def test_lookup_non_existing(apiobj):
+    assert apiobj.api.lookup((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):
-    import_date = dt.datetime(2022, 12, 7, 14, 14, 46, 0)
+def test_lookup_single_placex(apiobj, idobj):
     apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
                      class_='highway', type='residential',
                      name={'name': 'Road'}, address={'city': 'Barrow'},
@@ -28,22 +34,20 @@ def test_lookup_in_placex(apiobj, idobj):
                      rank_search=27, rank_address=26,
                      importance=0.01,
                      centroid=(23, 34),
-                     indexed_date=import_date,
                      geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)')
 
-    result = apiobj.api.lookup(idobj, napi.LookupDetails())
+    result = apiobj.api.lookup([idobj])
 
-    assert result is not None
+    assert len(result) == 1
+
+    result = result[0]
 
     assert result.source_table.name == 'PLACEX'
     assert result.category == ('highway', 'residential')
     assert result.centroid == (pytest.approx(23.0), pytest.approx(34.0))
 
     assert result.place_id == 332
-    assert result.parent_place_id == 34
-    assert result.linked_place_id == 55
     assert result.osm_object == ('W', 4)
-    assert result.admin_level == 15
 
     assert result.names == {'name': 'Road'}
     assert result.address == {'city': 'Barrow'}
@@ -58,7 +62,6 @@ def test_lookup_in_placex(apiobj, idobj):
     assert result.importance == pytest.approx(0.01)
 
     assert result.country_code == 'gb'
-    assert result.indexed_date == import_date.replace(tzinfo=dt.timezone.utc)
 
     assert result.address_rows is None
     assert result.linked_rows is None
@@ -66,519 +69,34 @@ def test_lookup_in_placex(apiobj, idobj):
     assert result.name_keywords is None
     assert result.address_keywords is None
 
-    assert result.geometry == {'type': 'ST_LineString'}
+    assert result.geometry == {}
 
 
-def test_lookup_in_placex_minimal_info(apiobj):
-    import_date = dt.datetime(2022, 12, 7, 14, 14, 46, 0)
+def test_lookup_multiple_places(apiobj):
     apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
                      class_='highway', type='residential',
-                     admin_level=15,
+                     name={'name': 'Road'}, address={'city': 'Barrow'},
+                     extratags={'surface': 'paved'},
+                     parent_place_id=34, linked_place_id=55,
+                     admin_level=15, country_code='gb',
+                     housenumber='4',
+                     postcode='34425', wikipedia='en:Faa',
                      rank_search=27, rank_address=26,
+                     importance=0.01,
                      centroid=(23, 34),
-                     indexed_date=import_date,
                      geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)')
-
-    result = apiobj.api.lookup(napi.PlaceID(332), napi.LookupDetails())
-
-    assert result is not None
-
-    assert result.source_table.name == 'PLACEX'
-    assert result.category == ('highway', 'residential')
-    assert result.centroid == (pytest.approx(23.0), pytest.approx(34.0))
-
-    assert result.place_id == 332
-    assert result.parent_place_id is None
-    assert result.linked_place_id is None
-    assert result.osm_object == ('W', 4)
-    assert result.admin_level == 15
-
-    assert result.names is None
-    assert result.address is None
-    assert result.extratags is None
-
-    assert result.housenumber is None
-    assert result.postcode is None
-    assert result.wikipedia is None
-
-    assert result.rank_search == 27
-    assert result.rank_address == 26
-    assert result.importance is None
-
-    assert result.country_code is None
-    assert result.indexed_date == import_date.replace(tzinfo=dt.timezone.utc)
-
-    assert result.address_rows is None
-    assert result.linked_rows is None
-    assert result.parented_rows is None
-    assert result.name_keywords is None
-    assert result.address_keywords is None
-
-    assert result.geometry == {'type': 'ST_LineString'}
-
-
-def test_lookup_in_placex_with_geometry(apiobj):
-    apiobj.add_placex(place_id=332,
-                      geometry='LINESTRING(23 34, 23.1 34)')
-
-    result = apiobj.api.lookup(napi.PlaceID(332),
-                               napi.LookupDetails(geometry_output=napi.GeometryFormat.GEOJSON))
-
-    assert result.geometry == {'geojson': '{"type":"LineString","coordinates":[[23,34],[23.1,34]]}'}
-
-
-def test_lookup_placex_with_address_details(apiobj):
-    apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
-                     class_='highway', type='residential',  name='Street',
-                     country_code='pl',
-                     rank_search=27, rank_address=26)
-    apiobj.add_address_placex(332, fromarea=False, isaddress=False,
-                              distance=0.0034,
-                              place_id=1000, osm_type='N', osm_id=3333,
-                              class_='place', type='suburb', name='Smallplace',
-                              country_code='pl', admin_level=13,
-                              rank_search=24, rank_address=23)
-    apiobj.add_address_placex(332, fromarea=True, isaddress=True,
-                              place_id=1001, osm_type='N', osm_id=3334,
-                              class_='place', type='city', name='Bigplace',
-                              country_code='pl',
-                              rank_search=17, rank_address=16)
-
-    result = apiobj.api.lookup(napi.PlaceID(332),
-                               napi.LookupDetails(address_details=True))
-
-    assert result.address_rows == [
-               napi.AddressLine(place_id=332, osm_object=('W', 4),
-                                category=('highway', 'residential'),
-                                names={'name': 'Street'}, extratags={},
-                                admin_level=15, fromarea=True, isaddress=True,
-                                rank_address=26, distance=0.0),
-               napi.AddressLine(place_id=1000, osm_object=('N', 3333),
-                                category=('place', 'suburb'),
-                                names={'name': 'Smallplace'}, extratags={},
-                                admin_level=13, fromarea=False, isaddress=True,
-                                rank_address=23, distance=0.0034),
-               napi.AddressLine(place_id=1001, osm_object=('N', 3334),
-                                category=('place', 'city'),
-                                names={'name': 'Bigplace'}, extratags={},
-                                admin_level=15, fromarea=True, isaddress=True,
-                                rank_address=16, distance=0.0),
-               napi.AddressLine(place_id=None, osm_object=None,
-                                category=('place', 'country_code'),
-                                names={'ref': 'pl'}, extratags={},
-                                admin_level=None, fromarea=True, isaddress=False,
-                                rank_address=4, distance=0.0)
-           ]
-
-
-def test_lookup_place_with_linked_places_none_existing(apiobj):
-    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.lookup(napi.PlaceID(332),
-                               napi.LookupDetails(linked_places=True))
-
-    assert result.linked_rows == []
-
-
-def test_lookup_place_with_linked_places_existing(apiobj):
-    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=1001, osm_type='W', osm_id=5,
-                     class_='highway', type='residential',  name='Street',
-                     country_code='pl', linked_place_id=332,
-                     rank_search=27, rank_address=26)
-    apiobj.add_placex(place_id=1002, osm_type='W', osm_id=6,
-                     class_='highway', type='residential',  name='Street',
-                     country_code='pl', linked_place_id=332,
-                     rank_search=27, rank_address=26)
-
-    result = apiobj.api.lookup(napi.PlaceID(332),
-                               napi.LookupDetails(linked_places=True))
-
-    assert result.linked_rows == [
-               napi.AddressLine(place_id=1001, osm_object=('W', 5),
-                                category=('highway', 'residential'),
-                                names={'name': 'Street'}, extratags={},
-                                admin_level=15, fromarea=False, isaddress=True,
-                                rank_address=26, distance=0.0),
-               napi.AddressLine(place_id=1002, osm_object=('W', 6),
-                                category=('highway', 'residential'),
-                                names={'name': 'Street'}, extratags={},
-                                admin_level=15, fromarea=False, isaddress=True,
-                                rank_address=26, distance=0.0),
-    ]
-
-
-def test_lookup_place_with_parented_places_not_existing(apiobj):
-    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.lookup(napi.PlaceID(332),
-                               napi.LookupDetails(parented_places=True))
-
-    assert result.parented_rows == []
-
-
-def test_lookup_place_with_parented_places_existing(apiobj):
-    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=1001, osm_type='N', osm_id=5,
-                     class_='place', type='house', housenumber='23',
-                     country_code='pl', parent_place_id=332,
-                     rank_search=30, rank_address=30)
-    apiobj.add_placex(place_id=1002, osm_type='W', osm_id=6,
-                     class_='highway', type='residential',  name='Street',
-                     country_code='pl', parent_place_id=332,
-                     rank_search=27, rank_address=26)
-
-    result = apiobj.api.lookup(napi.PlaceID(332),
-                               napi.LookupDetails(parented_places=True))
-
-    assert result.parented_rows == [
-               napi.AddressLine(place_id=1001, osm_object=('N', 5),
-                                category=('place', 'house'),
-                                names={'housenumber': '23'}, extratags={},
-                                admin_level=15, fromarea=False, isaddress=True,
-                                rank_address=30, distance=0.0),
-    ]
-
-
-@pytest.mark.parametrize('idobj', (napi.PlaceID(4924), napi.OsmID('W', 9928)))
-def test_lookup_in_osmline(apiobj, 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,
                        startnumber=1, endnumber=4, step=1,
                        country_code='gb', postcode='34425',
                        address={'city': 'Big'},
-                       indexed_date=import_date,
                        geometry='LINESTRING(23 34, 23 35)')
 
-    result = apiobj.api.lookup(idobj, napi.LookupDetails())
-
-    assert result is not None
-
-    assert result.source_table.name == 'OSMLINE'
-    assert result.category == ('place', 'houses')
-    assert result.centroid == (pytest.approx(23.0), pytest.approx(34.5))
-
-    assert result.place_id == 4924
-    assert result.parent_place_id == 12
-    assert result.linked_place_id is None
-    assert result.osm_object == ('W', 9928)
-    assert result.admin_level == 15
-
-    assert result.names is None
-    assert result.address == {'city': 'Big'}
-    assert result.extratags == {'startnumber': '1', 'endnumber': '4', 'step': '1'}
-
-    assert result.housenumber is None
-    assert result.postcode == '34425'
-    assert result.wikipedia is None
-
-    assert result.rank_search == 30
-    assert result.rank_address == 30
-    assert result.importance is None
-
-    assert result.country_code == 'gb'
-    assert result.indexed_date == import_date.replace(tzinfo=dt.timezone.utc)
-
-    assert result.address_rows is None
-    assert result.linked_rows is None
-    assert result.parented_rows is None
-    assert result.name_keywords is None
-    assert result.address_keywords is None
-
-    assert result.geometry == {'type': 'ST_LineString'}
-
-
-def test_lookup_in_osmline_split_interpolation(apiobj):
-    apiobj.add_osmline(place_id=1000, osm_id=9,
-                       startnumber=2, endnumber=4, step=1)
-    apiobj.add_osmline(place_id=1001, osm_id=9,
-                       startnumber=6, endnumber=9, step=1)
-    apiobj.add_osmline(place_id=1002, osm_id=9,
-                       startnumber=11, endnumber=20, step=1)
-
-    for i in range(1, 6):
-        result = apiobj.api.lookup(napi.OsmID('W', 9, str(i)), napi.LookupDetails())
-        assert result.place_id == 1000
-    for i in range(7, 11):
-        result = apiobj.api.lookup(napi.OsmID('W', 9, str(i)), napi.LookupDetails())
-        assert result.place_id == 1001
-    for i in range(12, 22):
-        result = apiobj.api.lookup(napi.OsmID('W', 9, str(i)), napi.LookupDetails())
-        assert result.place_id == 1002
-
-
-def test_lookup_osmline_with_address_details(apiobj):
-    apiobj.add_osmline(place_id=9000, osm_id=9,
-                       startnumber=2, endnumber=4, step=1,
-                       parent_place_id=332)
-    apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
-                     class_='highway', type='residential',  name='Street',
-                     country_code='pl',
-                     rank_search=27, rank_address=26)
-    apiobj.add_address_placex(332, fromarea=False, isaddress=False,
-                              distance=0.0034,
-                              place_id=1000, osm_type='N', osm_id=3333,
-                              class_='place', type='suburb', name='Smallplace',
-                              country_code='pl', admin_level=13,
-                              rank_search=24, rank_address=23)
-    apiobj.add_address_placex(332, fromarea=True, isaddress=True,
-                              place_id=1001, osm_type='N', osm_id=3334,
-                              class_='place', type='city', name='Bigplace',
-                              country_code='pl',
-                              rank_search=17, rank_address=16)
-
-    result = apiobj.api.lookup(napi.PlaceID(9000),
-                               napi.LookupDetails(address_details=True))
-
-    assert result.address_rows == [
-               napi.AddressLine(place_id=None, osm_object=None,
-                                category=('place', 'house_number'),
-                                names={'ref': '2'}, extratags={},
-                                admin_level=None, fromarea=True, isaddress=True,
-                                rank_address=28, distance=0.0),
-               napi.AddressLine(place_id=332, osm_object=('W', 4),
-                                category=('highway', 'residential'),
-                                names={'name': 'Street'}, extratags={},
-                                admin_level=15, fromarea=True, isaddress=True,
-                                rank_address=26, distance=0.0),
-               napi.AddressLine(place_id=1000, osm_object=('N', 3333),
-                                category=('place', 'suburb'),
-                                names={'name': 'Smallplace'}, extratags={},
-                                admin_level=13, fromarea=False, isaddress=True,
-                                rank_address=23, distance=0.0034),
-               napi.AddressLine(place_id=1001, osm_object=('N', 3334),
-                                category=('place', 'city'),
-                                names={'name': 'Bigplace'}, extratags={},
-                                admin_level=15, fromarea=True, isaddress=True,
-                                rank_address=16, distance=0.0),
-               napi.AddressLine(place_id=None, osm_object=None,
-                                category=('place', 'country_code'),
-                                names={'ref': 'pl'}, extratags={},
-                                admin_level=None, fromarea=True, isaddress=False,
-                                rank_address=4, distance=0.0)
-           ]
-
-
-def test_lookup_in_tiger(apiobj):
-    apiobj.add_tiger(place_id=4924,
-                     parent_place_id=12,
-                     startnumber=1, endnumber=4, step=1,
-                     postcode='34425',
-                     geometry='LINESTRING(23 34, 23 35)')
-    apiobj.add_placex(place_id=12,
-                      category=('highway', 'residential'),
-                      osm_type='W', osm_id=6601223,
-                      geometry='LINESTRING(23 34, 23 35)')
-
-    result = apiobj.api.lookup(napi.PlaceID(4924), napi.LookupDetails())
-
-    assert result is not None
-
-    assert result.source_table.name == 'TIGER'
-    assert result.category == ('place', 'houses')
-    assert result.centroid == (pytest.approx(23.0), pytest.approx(34.5))
-
-    assert result.place_id == 4924
-    assert result.parent_place_id == 12
-    assert result.linked_place_id is None
-    assert result.osm_object == ('W', 6601223)
-    assert result.admin_level == 15
-
-    assert result.names is None
-    assert result.address is None
-    assert result.extratags == {'startnumber': '1', 'endnumber': '4', 'step': '1'}
-
-    assert result.housenumber is None
-    assert result.postcode == '34425'
-    assert result.wikipedia is None
-
-    assert result.rank_search == 30
-    assert result.rank_address == 30
-    assert result.importance is None
-
-    assert result.country_code == 'us'
-    assert result.indexed_date is None
-
-    assert result.address_rows is None
-    assert result.linked_rows is None
-    assert result.parented_rows is None
-    assert result.name_keywords is None
-    assert result.address_keywords is None
-
-    assert result.geometry == {'type': 'ST_LineString'}
-
-
-def test_lookup_tiger_with_address_details(apiobj):
-    apiobj.add_tiger(place_id=9000,
-                     startnumber=2, endnumber=4, step=1,
-                     parent_place_id=332)
-    apiobj.add_placex(place_id=332, osm_type='W', osm_id=4,
-                     class_='highway', type='residential',  name='Street',
-                     country_code='us',
-                     rank_search=27, rank_address=26)
-    apiobj.add_address_placex(332, fromarea=False, isaddress=False,
-                              distance=0.0034,
-                              place_id=1000, osm_type='N', osm_id=3333,
-                              class_='place', type='suburb', name='Smallplace',
-                              country_code='us', admin_level=13,
-                              rank_search=24, rank_address=23)
-    apiobj.add_address_placex(332, fromarea=True, isaddress=True,
-                              place_id=1001, osm_type='N', osm_id=3334,
-                              class_='place', type='city', name='Bigplace',
-                              country_code='us',
-                              rank_search=17, rank_address=16)
-
-    result = apiobj.api.lookup(napi.PlaceID(9000),
-                               napi.LookupDetails(address_details=True))
-
-    assert result.address_rows == [
-               napi.AddressLine(place_id=None, osm_object=None,
-                                category=('place', 'house_number'),
-                                names={'ref': '2'}, extratags={},
-                                admin_level=None, fromarea=True, isaddress=True,
-                                rank_address=28, distance=0.0),
-               napi.AddressLine(place_id=332, osm_object=('W', 4),
-                                category=('highway', 'residential'),
-                                names={'name': 'Street'}, extratags={},
-                                admin_level=15, fromarea=True, isaddress=True,
-                                rank_address=26, distance=0.0),
-               napi.AddressLine(place_id=1000, osm_object=('N', 3333),
-                                category=('place', 'suburb'),
-                                names={'name': 'Smallplace'}, extratags={},
-                                admin_level=13, fromarea=False, isaddress=True,
-                                rank_address=23, distance=0.0034),
-               napi.AddressLine(place_id=1001, osm_object=('N', 3334),
-                                category=('place', 'city'),
-                                names={'name': 'Bigplace'}, extratags={},
-                                admin_level=15, fromarea=True, isaddress=True,
-                                rank_address=16, distance=0.0),
-               napi.AddressLine(place_id=None, osm_object=None,
-                                category=('place', 'country_code'),
-                                names={'ref': 'us'}, extratags={},
-                                admin_level=None, fromarea=True, isaddress=False,
-                                rank_address=4, distance=0.0)
-           ]
-
-
-def test_lookup_in_postcode(apiobj):
-    import_date = dt.datetime(2022, 12, 7, 14, 14, 46, 0)
-    apiobj.add_postcode(place_id=554,
-                        parent_place_id=152,
-                        postcode='34 425',
-                        country_code='gb',
-                        rank_search=20, rank_address=22,
-                        indexed_date=import_date,
-                        geometry='POINT(-9.45 5.6)')
-
-    result = apiobj.api.lookup(napi.PlaceID(554), napi.LookupDetails())
-
-    assert result is not None
-
-    assert result.source_table.name == 'POSTCODE'
-    assert result.category == ('place', 'postcode')
-    assert result.centroid == (pytest.approx(-9.45), pytest.approx(5.6))
-
-    assert result.place_id == 554
-    assert result.parent_place_id == 152
-    assert result.linked_place_id is None
-    assert result.osm_object is None
-    assert result.admin_level == 15
-
-    assert result.names == {'ref': '34 425'}
-    assert result.address is None
-    assert result.extratags is None
-
-    assert result.housenumber is None
-    assert result.postcode is None
-    assert result.wikipedia is None
-
-    assert result.rank_search == 20
-    assert result.rank_address == 22
-    assert result.importance is None
-
-    assert result.country_code == 'gb'
-    assert result.indexed_date == import_date.replace(tzinfo=dt.timezone.utc)
-
-    assert result.address_rows is None
-    assert result.linked_rows is None
-    assert result.parented_rows is None
-    assert result.name_keywords is None
-    assert result.address_keywords is None
-
-    assert result.geometry == {'type': 'ST_Point'}
-
-
-def test_lookup_postcode_with_address_details(apiobj):
-    apiobj.add_postcode(place_id=9000,
-                        parent_place_id=332,
-                        postcode='34 425',
-                        country_code='gb',
-                        rank_search=25, rank_address=25)
-    apiobj.add_placex(place_id=332, osm_type='N', osm_id=3333,
-                      class_='place', type='suburb',  name='Smallplace',
-                      country_code='gb', admin_level=13,
-                      rank_search=24, rank_address=23)
-    apiobj.add_address_placex(332, fromarea=True, isaddress=True,
-                              place_id=1001, osm_type='N', osm_id=3334,
-                              class_='place', type='city', name='Bigplace',
-                              country_code='gb',
-                              rank_search=17, rank_address=16)
-
-    result = apiobj.api.lookup(napi.PlaceID(9000),
-                               napi.LookupDetails(address_details=True))
-
-    assert result.address_rows == [
-               napi.AddressLine(place_id=332, osm_object=('N', 3333),
-                                category=('place', 'suburb'),
-                                names={'name': 'Smallplace'}, extratags={},
-                                admin_level=13, fromarea=True, isaddress=True,
-                                rank_address=23, distance=0.0),
-               napi.AddressLine(place_id=1001, osm_object=('N', 3334),
-                                category=('place', 'city'),
-                                names={'name': 'Bigplace'}, extratags={},
-                                admin_level=15, fromarea=True, isaddress=True,
-                                rank_address=16, distance=0.0),
-               napi.AddressLine(place_id=None, osm_object=None,
-                                category=('place', 'postcode'),
-                                names={'ref': '34 425'}, extratags={},
-                                admin_level=None, fromarea=False, isaddress=True,
-                                rank_address=5, distance=0.0),
-               napi.AddressLine(place_id=None, osm_object=None,
-                                category=('place', 'country_code'),
-                                names={'ref': 'gb'}, extratags={},
-                                admin_level=None, fromarea=True, isaddress=False,
-                                rank_address=4, distance=0.0)
-           ]
-
-@pytest.mark.parametrize('objid', [napi.PlaceID(1736),
-                                   napi.OsmID('W', 55),
-                                   napi.OsmID('N', 55, 'amenity')])
-def test_lookup_missing_object(apiobj, objid):
-    apiobj.add_placex(place_id=1, osm_type='N', osm_id=55,
-                      class_='place', type='suburb')
-
-    assert apiobj.api.lookup(objid, napi.LookupDetails()) is None
 
+    result = apiobj.api.lookup((napi.OsmID('W', 1),
+                                napi.OsmID('W', 4),
+                                napi.OsmID('W', 9928)), napi.LookupDetails())
 
-@pytest.mark.parametrize('gtype', (napi.GeometryFormat.KML,
-                                    napi.GeometryFormat.SVG,
-                                    napi.GeometryFormat.TEXT))
-def test_lookup_unsupported_geometry(apiobj, gtype):
-    apiobj.add_placex(place_id=332)
+    assert len(result) == 2
 
-    with pytest.raises(ValueError):
-        apiobj.api.lookup(napi.PlaceID(332),
-                          napi.LookupDetails(geometry_output=gtype))
+    assert set(r.place_id for r in result) == {332, 4924}
index a374bdcf51ca30daaac4f237548f4e1dca4095d5..c0ca69dd6a9931dae142d4db7bc9003adc508288 100644 (file)
@@ -335,7 +335,7 @@ class TestDetailsEndpoint:
             self.lookup_args.extend(args[1:])
             return self.result
 
-        monkeypatch.setattr(napi.NominatimAPIAsync, 'lookup', _lookup)
+        monkeypatch.setattr(napi.NominatimAPIAsync, 'details', _lookup)
 
 
     @pytest.mark.asyncio
@@ -384,3 +384,63 @@ class TestDetailsEndpoint:
 
         with pytest.raises(FakeError, match='^404 -- .*found'):
             await glue.details_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a)
+
+
+# lookup_endpoint()
+
+class TestLookupEndpoint:
+
+    @pytest.fixture(autouse=True)
+    def patch_lookup_func(self, monkeypatch):
+        self.results = [napi.SearchResult(napi.SourceTable.PLACEX,
+                                          ('place', 'thing'),
+                                          napi.Point(1.0, 2.0))]
+        async def _lookup(*args, **kwargs):
+            return napi.SearchResults(self.results)
+
+        monkeypatch.setattr(napi.NominatimAPIAsync, 'lookup', _lookup)
+
+
+    @pytest.mark.asyncio
+    async def test_lookup_no_params(self):
+        a = FakeAdaptor()
+        a.params['format'] = 'json'
+
+        res = await glue.lookup_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a)
+
+        assert res.output == '[]'
+
+
+    @pytest.mark.asyncio
+    @pytest.mark.parametrize('param', ['w', 'bad', ''])
+    async def test_lookup_bad_params(self, param):
+        a = FakeAdaptor()
+        a.params['format'] = 'json'
+        a.params['osm_ids'] = f'W34,{param},N33333'
+
+        res = await glue.lookup_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a)
+
+        assert len(json.loads(res.output)) == 1
+
+
+    @pytest.mark.asyncio
+    @pytest.mark.parametrize('param', ['p234234', '4563'])
+    async def test_lookup_bad_osm_type(self, param):
+        a = FakeAdaptor()
+        a.params['format'] = 'json'
+        a.params['osm_ids'] = f'W34,{param},N33333'
+
+        res = await glue.lookup_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a)
+
+        assert len(json.loads(res.output)) == 1
+
+
+    @pytest.mark.asyncio
+    async def test_lookup_working(self):
+        a = FakeAdaptor()
+        a.params['format'] = 'json'
+        a.params['osm_ids'] = 'N23,W34'
+
+        res = await glue.lookup_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a)
+
+        assert len(json.loads(res.output)) == 1
index cff83cef0888f701a1de79ab7830a127ae2f150f..e8c447aa5f722732770a09fab26283d0838e13fe 100644 (file)
@@ -23,8 +23,7 @@ def test_no_api_without_phpcgi(endpoint):
 
 
 @pytest.mark.parametrize("params", [('search', '--query', 'new'),
-                                    ('search', '--city', 'Berlin'),
-                                    ('lookup', '--id', 'N1')])
+                                    ('search', '--city', 'Berlin')])
 class TestCliApiCallPhp:
 
     @pytest.fixture(autouse=True)
@@ -81,7 +80,7 @@ class TestCliDetailsCall:
         result = napi.DetailedResult(napi.SourceTable.PLACEX, ('place', 'thing'),
                                      napi.Point(1.0, -3.0))
 
-        monkeypatch.setattr(napi.NominatimAPI, 'lookup',
+        monkeypatch.setattr(napi.NominatimAPI, 'details',
                             lambda *args: result)
 
     @pytest.mark.parametrize("params", [('--node', '1'),
@@ -156,32 +155,49 @@ class TestCliReverseCall:
         assert out['name'] == 'Nom'
 
 
-QUERY_PARAMS = {
- 'search': ('--query', 'somewhere'),
- 'reverse': ('--lat', '20', '--lon', '30'),
- 'lookup': ('--id', 'R345345'),
- 'details': ('--node', '324')
-}
+class TestCliLookupCall:
+
+    @pytest.fixture(autouse=True)
+    def setup_lookup_mock(self, monkeypatch):
+        result = napi.SearchResult(napi.SourceTable.PLACEX, ('place', 'thing'),
+                                    napi.Point(1.0, -3.0),
+                                    names={'name':'Name', 'name:fr': 'Nom'},
+                                    extratags={'extra':'Extra'})
+
+        monkeypatch.setattr(napi.NominatimAPI, 'lookup',
+                            lambda *args: napi.SearchResults([result]))
+
+    def test_lookup_simple(self, cli_call, tmp_path, capsys):
+        result = cli_call('lookup', '--project-dir', str(tmp_path),
+                          '--id', 'N34')
+
+        assert result == 0
+
+        out = json.loads(capsys.readouterr().out)
+        assert len(out) == 1
+        assert out[0]['name'] == 'Name'
+        assert 'address' not in out[0]
+        assert 'extratags' not in out[0]
+        assert 'namedetails' not in out[0]
+
 
-@pytest.mark.parametrize("endpoint", (('search', 'lookup')))
 class TestCliApiCommonParameters:
 
     @pytest.fixture(autouse=True)
-    def setup_website_dir(self, cli_call, project_env, endpoint):
-        self.endpoint = endpoint
+    def setup_website_dir(self, cli_call, project_env):
         self.cli_call = cli_call
         self.project_dir = project_env.project_dir
         (self.project_dir / 'website').mkdir()
 
 
     def expect_param(self, param, expected):
-        (self.project_dir / 'website' / (self.endpoint + '.php')).write_text(f"""<?php
+        (self.project_dir / 'website' / ('search.php')).write_text(f"""<?php
         exit($_GET['{param}']  == '{expected}' ? 0 : 10);
         """)
 
 
     def call_nominatim(self, *params):
-        return self.cli_call(self.endpoint, *QUERY_PARAMS[self.endpoint],
+        return self.cli_call('search', '--query', 'somewhere',
                              '--project-dir', str(self.project_dir), *params)
 
 
@@ -221,7 +237,7 @@ def test_cli_search_param_bounded(cli_call, project_env):
         exit($_GET['bounded']  == '1' ? 0 : 10);
         """)
 
-    assert cli_call('search', *QUERY_PARAMS['search'], '--project-dir', str(project_env.project_dir),
+    assert cli_call('search', '--query', 'somewhere', '--project-dir', str(project_env.project_dir),
                     '--bounded') == 0
 
 
@@ -232,5 +248,5 @@ def test_cli_search_param_dedupe(cli_call, project_env):
         exit($_GET['dedupe']  == '0' ? 0 : 10);
         """)
 
-    assert cli_call('search', *QUERY_PARAMS['search'], '--project-dir', str(project_env.project_dir),
+    assert cli_call('search', '--query', 'somewhere', '--project-dir', str(project_env.project_dir),
                     '--no-dedupe') == 0