newend INTEGER;
moddiff SMALLINT;
linegeo GEOMETRY;
- splitline GEOMETRY;
+ splitpoint FLOAT;
sectiongeo GEOMETRY;
postcode TEXT;
stepmod SMALLINT;
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
-- 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;
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;
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;
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)
"""
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
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:
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,
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,
"""
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.
"""
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)
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.
"""
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)
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
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
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
"""
+@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_')}
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
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
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')
"""
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))\
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.
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.
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.
"""
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)\
"""
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 = {}
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:
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:
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.
"""
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())
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
'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)
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
ROUTES = [
('status', status_endpoint),
('details', details_endpoint),
- ('reverse', reverse_endpoint)
+ ('reverse', reverse_endpoint),
+ ('lookup', lookup_endpoint)
]
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:
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(
| 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
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
| 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 |
--- /dev/null
+# 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))
"""
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'},
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'}
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.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}
self.lookup_args.extend(args[1:])
return self.result
- monkeypatch.setattr(napi.NominatimAPIAsync, 'lookup', _lookup)
+ monkeypatch.setattr(napi.NominatimAPIAsync, 'details', _lookup)
@pytest.mark.asyncio
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
@pytest.mark.parametrize("params", [('search', '--query', 'new'),
- ('search', '--city', 'Berlin'),
- ('lookup', '--id', 'N1')])
+ ('search', '--city', 'Berlin')])
class TestCliApiCallPhp:
@pytest.fixture(autouse=True)
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'),
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)
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
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