mirror of
https://github.com/osm-search/Nominatim.git
synced 2026-02-26 11:08:13 +00:00
Merge pull request #3063 from lonvia/variable-parameters
Rework how search parameters are handed to the Python API
This commit is contained in:
@@ -23,7 +23,6 @@ from .types import (PlaceID as PlaceID,
|
|||||||
Point as Point,
|
Point as Point,
|
||||||
Bbox as Bbox,
|
Bbox as Bbox,
|
||||||
GeometryFormat as GeometryFormat,
|
GeometryFormat as GeometryFormat,
|
||||||
LookupDetails as LookupDetails,
|
|
||||||
DataLayer as DataLayer)
|
DataLayer as DataLayer)
|
||||||
from .results import (SourceTable as SourceTable,
|
from .results import (SourceTable as SourceTable,
|
||||||
AddressLine as AddressLine,
|
AddressLine as AddressLine,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from nominatim.api.connection import SearchConnection
|
|||||||
from nominatim.api.status import get_status, StatusResult
|
from nominatim.api.status import get_status, StatusResult
|
||||||
from nominatim.api.lookup import get_detailed_place, get_simple_place
|
from nominatim.api.lookup import get_detailed_place, get_simple_place
|
||||||
from nominatim.api.reverse import ReverseGeocoder
|
from nominatim.api.reverse import ReverseGeocoder
|
||||||
from nominatim.api.types import PlaceRef, LookupDetails, AnyPoint, DataLayer
|
import nominatim.api.types as ntyp
|
||||||
from nominatim.api.results import DetailedResult, ReverseResult, SearchResults
|
from nominatim.api.results import DetailedResult, ReverseResult, SearchResults
|
||||||
|
|
||||||
|
|
||||||
@@ -128,32 +128,28 @@ class NominatimAPIAsync:
|
|||||||
return status
|
return status
|
||||||
|
|
||||||
|
|
||||||
async def details(self, place: PlaceRef,
|
async def details(self, place: ntyp.PlaceRef, **params: Any) -> Optional[DetailedResult]:
|
||||||
details: Optional[LookupDetails] = None) -> Optional[DetailedResult]:
|
|
||||||
""" Get detailed information about a place in the database.
|
""" Get detailed information about a place in the database.
|
||||||
|
|
||||||
Returns None if there is no entry under the given ID.
|
Returns None if there is no entry under the given ID.
|
||||||
"""
|
"""
|
||||||
async with self.begin() as conn:
|
async with self.begin() as conn:
|
||||||
return await get_detailed_place(conn, place, details or LookupDetails())
|
return await get_detailed_place(conn, place,
|
||||||
|
ntyp.LookupDetails.from_kwargs(params))
|
||||||
|
|
||||||
|
|
||||||
async def lookup(self, places: Sequence[PlaceRef],
|
async def lookup(self, places: Sequence[ntyp.PlaceRef], **params: Any) -> SearchResults:
|
||||||
details: Optional[LookupDetails] = None) -> SearchResults:
|
|
||||||
""" Get simple information about a list of places.
|
""" Get simple information about a list of places.
|
||||||
|
|
||||||
Returns a list of place information for all IDs that were found.
|
Returns a list of place information for all IDs that were found.
|
||||||
"""
|
"""
|
||||||
if details is None:
|
details = ntyp.LookupDetails.from_kwargs(params)
|
||||||
details = LookupDetails()
|
|
||||||
async with self.begin() as conn:
|
async with self.begin() as conn:
|
||||||
return SearchResults(filter(None,
|
return SearchResults(filter(None,
|
||||||
[await get_simple_place(conn, p, details) for p in places]))
|
[await get_simple_place(conn, p, details) for p in places]))
|
||||||
|
|
||||||
|
|
||||||
async def reverse(self, coord: AnyPoint, max_rank: Optional[int] = None,
|
async def reverse(self, coord: ntyp.AnyPoint, **params: Any) -> Optional[ReverseResult]:
|
||||||
layer: Optional[DataLayer] = None,
|
|
||||||
details: Optional[LookupDetails] = None) -> Optional[ReverseResult]:
|
|
||||||
""" Find a place by its coordinates. Also known as reverse geocoding.
|
""" Find a place by its coordinates. Also known as reverse geocoding.
|
||||||
|
|
||||||
Returns the closest result that can be found or None if
|
Returns the closest result that can be found or None if
|
||||||
@@ -164,14 +160,8 @@ class NominatimAPIAsync:
|
|||||||
# There are no results to be expected outside valid coordinates.
|
# There are no results to be expected outside valid coordinates.
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if layer is None:
|
|
||||||
layer = DataLayer.ADDRESS | DataLayer.POI
|
|
||||||
|
|
||||||
max_rank = max(0, min(max_rank or 30, 30))
|
|
||||||
|
|
||||||
async with self.begin() as conn:
|
async with self.begin() as conn:
|
||||||
geocoder = ReverseGeocoder(conn, max_rank, layer,
|
geocoder = ReverseGeocoder(conn, ntyp.ReverseDetails.from_kwargs(params))
|
||||||
details or LookupDetails())
|
|
||||||
return await geocoder.lookup(coord)
|
return await geocoder.lookup(coord)
|
||||||
|
|
||||||
|
|
||||||
@@ -206,29 +196,24 @@ class NominatimAPI:
|
|||||||
return self._loop.run_until_complete(self._async_api.status())
|
return self._loop.run_until_complete(self._async_api.status())
|
||||||
|
|
||||||
|
|
||||||
def details(self, place: PlaceRef,
|
def details(self, place: ntyp.PlaceRef, **params: Any) -> Optional[DetailedResult]:
|
||||||
details: Optional[LookupDetails] = None) -> Optional[DetailedResult]:
|
|
||||||
""" Get detailed information about a place in the database.
|
""" Get detailed information about a place in the database.
|
||||||
"""
|
"""
|
||||||
return self._loop.run_until_complete(self._async_api.details(place, details))
|
return self._loop.run_until_complete(self._async_api.details(place, **params))
|
||||||
|
|
||||||
|
|
||||||
def lookup(self, places: Sequence[PlaceRef],
|
def lookup(self, places: Sequence[ntyp.PlaceRef], **params: Any) -> SearchResults:
|
||||||
details: Optional[LookupDetails] = None) -> SearchResults:
|
|
||||||
""" Get simple information about a list of places.
|
""" Get simple information about a list of places.
|
||||||
|
|
||||||
Returns a list of place information for all IDs that were found.
|
Returns a list of place information for all IDs that were found.
|
||||||
"""
|
"""
|
||||||
return self._loop.run_until_complete(self._async_api.lookup(places, details))
|
return self._loop.run_until_complete(self._async_api.lookup(places, **params))
|
||||||
|
|
||||||
|
|
||||||
def reverse(self, coord: AnyPoint, max_rank: Optional[int] = None,
|
def reverse(self, coord: ntyp.AnyPoint, **params: Any) -> Optional[ReverseResult]:
|
||||||
layer: Optional[DataLayer] = None,
|
|
||||||
details: Optional[LookupDetails] = None) -> Optional[ReverseResult]:
|
|
||||||
""" Find a place by its coordinates. Also known as reverse geocoding.
|
""" Find a place by its coordinates. Also known as reverse geocoding.
|
||||||
|
|
||||||
Returns the closest result that can be found or None if
|
Returns the closest result that can be found or None if
|
||||||
no place matches the given criteria.
|
no place matches the given criteria.
|
||||||
"""
|
"""
|
||||||
return self._loop.run_until_complete(
|
return self._loop.run_until_complete(self._async_api.reverse(coord, **params))
|
||||||
self._async_api.reverse(coord, max_rank, layer, details))
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from nominatim.typing import SaColumn, SaSelect, SaFromClause, SaLabel, SaRow
|
|||||||
from nominatim.api.connection import SearchConnection
|
from nominatim.api.connection import SearchConnection
|
||||||
import nominatim.api.results as nres
|
import nominatim.api.results as nres
|
||||||
from nominatim.api.logging import log
|
from nominatim.api.logging import log
|
||||||
from nominatim.api.types import AnyPoint, DataLayer, LookupDetails, GeometryFormat, Bbox
|
from nominatim.api.types import AnyPoint, DataLayer, ReverseDetails, GeometryFormat, Bbox
|
||||||
|
|
||||||
# In SQLAlchemy expression which compare with NULL need to be expressed with
|
# In SQLAlchemy expression which compare with NULL need to be expressed with
|
||||||
# the equal sign.
|
# the equal sign.
|
||||||
@@ -87,23 +87,34 @@ class ReverseGeocoder:
|
|||||||
coordinate.
|
coordinate.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, conn: SearchConnection, max_rank: int, layer: DataLayer,
|
def __init__(self, conn: SearchConnection, params: ReverseDetails) -> None:
|
||||||
details: LookupDetails) -> None:
|
|
||||||
self.conn = conn
|
self.conn = conn
|
||||||
self.max_rank = max_rank
|
self.params = params
|
||||||
self.layer = layer
|
|
||||||
self.details = details
|
|
||||||
|
@property
|
||||||
|
def max_rank(self) -> int:
|
||||||
|
""" Return the maximum configured rank.
|
||||||
|
"""
|
||||||
|
return self.params.max_rank
|
||||||
|
|
||||||
|
|
||||||
|
def has_geometries(self) -> bool:
|
||||||
|
""" Check if any geometries are requested.
|
||||||
|
"""
|
||||||
|
return bool(self.params.geometry_output)
|
||||||
|
|
||||||
|
|
||||||
def layer_enabled(self, *layer: DataLayer) -> bool:
|
def layer_enabled(self, *layer: DataLayer) -> bool:
|
||||||
""" Return true when any of the given layer types are requested.
|
""" Return true when any of the given layer types are requested.
|
||||||
"""
|
"""
|
||||||
return any(self.layer & l for l in layer)
|
return any(self.params.layers & l for l in layer)
|
||||||
|
|
||||||
|
|
||||||
def layer_disabled(self, *layer: DataLayer) -> bool:
|
def layer_disabled(self, *layer: DataLayer) -> bool:
|
||||||
""" Return true when none of the given layer types is requested.
|
""" Return true when none of the given layer types is requested.
|
||||||
"""
|
"""
|
||||||
return not any(self.layer & l for l in layer)
|
return not any(self.params.layers & l for l in layer)
|
||||||
|
|
||||||
|
|
||||||
def has_feature_layers(self) -> bool:
|
def has_feature_layers(self) -> bool:
|
||||||
@@ -112,21 +123,21 @@ class ReverseGeocoder:
|
|||||||
return self.layer_enabled(DataLayer.RAILWAY, DataLayer.MANMADE, DataLayer.NATURAL)
|
return self.layer_enabled(DataLayer.RAILWAY, DataLayer.MANMADE, DataLayer.NATURAL)
|
||||||
|
|
||||||
def _add_geometry_columns(self, sql: SaSelect, col: SaColumn) -> SaSelect:
|
def _add_geometry_columns(self, sql: SaSelect, col: SaColumn) -> SaSelect:
|
||||||
if not self.details.geometry_output:
|
if not self.has_geometries():
|
||||||
return sql
|
return sql
|
||||||
|
|
||||||
out = []
|
out = []
|
||||||
|
|
||||||
if self.details.geometry_simplification > 0.0:
|
if self.params.geometry_simplification > 0.0:
|
||||||
col = col.ST_SimplifyPreserveTopology(self.details.geometry_simplification)
|
col = col.ST_SimplifyPreserveTopology(self.params.geometry_simplification)
|
||||||
|
|
||||||
if self.details.geometry_output & GeometryFormat.GEOJSON:
|
if self.params.geometry_output & GeometryFormat.GEOJSON:
|
||||||
out.append(col.ST_AsGeoJSON().label('geometry_geojson'))
|
out.append(col.ST_AsGeoJSON().label('geometry_geojson'))
|
||||||
if self.details.geometry_output & GeometryFormat.TEXT:
|
if self.params.geometry_output & GeometryFormat.TEXT:
|
||||||
out.append(col.ST_AsText().label('geometry_text'))
|
out.append(col.ST_AsText().label('geometry_text'))
|
||||||
if self.details.geometry_output & GeometryFormat.KML:
|
if self.params.geometry_output & GeometryFormat.KML:
|
||||||
out.append(col.ST_AsKML().label('geometry_kml'))
|
out.append(col.ST_AsKML().label('geometry_kml'))
|
||||||
if self.details.geometry_output & GeometryFormat.SVG:
|
if self.params.geometry_output & GeometryFormat.SVG:
|
||||||
out.append(col.ST_AsSVG().label('geometry_svg'))
|
out.append(col.ST_AsSVG().label('geometry_svg'))
|
||||||
|
|
||||||
return sql.add_columns(*out)
|
return sql.add_columns(*out)
|
||||||
@@ -224,7 +235,7 @@ class ReverseGeocoder:
|
|||||||
if parent_place_id is not None:
|
if parent_place_id is not None:
|
||||||
sql = sql.where(t.c.parent_place_id == parent_place_id)
|
sql = sql.where(t.c.parent_place_id == parent_place_id)
|
||||||
|
|
||||||
inner = sql.subquery()
|
inner = sql.subquery('ipol')
|
||||||
|
|
||||||
sql = sa.select(inner.c.place_id, inner.c.osm_id,
|
sql = sa.select(inner.c.place_id, inner.c.osm_id,
|
||||||
inner.c.parent_place_id, inner.c.address,
|
inner.c.parent_place_id, inner.c.address,
|
||||||
@@ -233,9 +244,9 @@ class ReverseGeocoder:
|
|||||||
inner.c.postcode, inner.c.country_code,
|
inner.c.postcode, inner.c.country_code,
|
||||||
inner.c.distance)
|
inner.c.distance)
|
||||||
|
|
||||||
if self.details.geometry_output:
|
if self.has_geometries():
|
||||||
sub = sql.subquery()
|
sub = sql.subquery('geom')
|
||||||
sql = self._add_geometry_columns(sql, sub.c.centroid)
|
sql = self._add_geometry_columns(sa.select(sub), sub.c.centroid)
|
||||||
|
|
||||||
return (await self.conn.execute(sql)).one_or_none()
|
return (await self.conn.execute(sql)).one_or_none()
|
||||||
|
|
||||||
@@ -252,7 +263,7 @@ class ReverseGeocoder:
|
|||||||
.where(t.c.parent_place_id == parent_place_id)\
|
.where(t.c.parent_place_id == parent_place_id)\
|
||||||
.order_by('distance')\
|
.order_by('distance')\
|
||||||
.limit(1)\
|
.limit(1)\
|
||||||
.subquery()
|
.subquery('tiger')
|
||||||
|
|
||||||
sql = sa.select(inner.c.place_id,
|
sql = sa.select(inner.c.place_id,
|
||||||
inner.c.parent_place_id,
|
inner.c.parent_place_id,
|
||||||
@@ -263,9 +274,9 @@ class ReverseGeocoder:
|
|||||||
inner.c.postcode,
|
inner.c.postcode,
|
||||||
inner.c.distance)
|
inner.c.distance)
|
||||||
|
|
||||||
if self.details.geometry_output:
|
if self.has_geometries():
|
||||||
sub = sql.subquery()
|
sub = sql.subquery('geom')
|
||||||
sql = self._add_geometry_columns(sql, sub.c.centroid)
|
sql = self._add_geometry_columns(sa.select(sub), sub.c.centroid)
|
||||||
|
|
||||||
return (await self.conn.execute(sql)).one_or_none()
|
return (await self.conn.execute(sql)).one_or_none()
|
||||||
|
|
||||||
@@ -345,7 +356,7 @@ class ReverseGeocoder:
|
|||||||
.where(t.c.type != 'postcode')\
|
.where(t.c.type != 'postcode')\
|
||||||
.order_by(sa.desc(t.c.rank_search))\
|
.order_by(sa.desc(t.c.rank_search))\
|
||||||
.limit(50)\
|
.limit(50)\
|
||||||
.subquery()
|
.subquery('area')
|
||||||
|
|
||||||
sql = _select_from_placex(inner)\
|
sql = _select_from_placex(inner)\
|
||||||
.where(inner.c.geometry.ST_Contains(wkt))\
|
.where(inner.c.geometry.ST_Contains(wkt))\
|
||||||
@@ -374,7 +385,7 @@ class ReverseGeocoder:
|
|||||||
.intersects(wkt))\
|
.intersects(wkt))\
|
||||||
.order_by(sa.desc(t.c.rank_search))\
|
.order_by(sa.desc(t.c.rank_search))\
|
||||||
.limit(50)\
|
.limit(50)\
|
||||||
.subquery()
|
.subquery('places')
|
||||||
|
|
||||||
touter = self.conn.t.placex.alias('outer')
|
touter = self.conn.t.placex.alias('outer')
|
||||||
sql = _select_from_placex(inner)\
|
sql = _select_from_placex(inner)\
|
||||||
@@ -514,9 +525,7 @@ class ReverseGeocoder:
|
|||||||
""" Look up a single coordinate. Returns the place information,
|
""" Look up a single coordinate. Returns the place information,
|
||||||
if a place was found near the coordinates or None otherwise.
|
if a place was found near the coordinates or None otherwise.
|
||||||
"""
|
"""
|
||||||
log().function('reverse_lookup',
|
log().function('reverse_lookup', coord=coord, params=self.params)
|
||||||
coord=coord, max_rank=self.max_rank,
|
|
||||||
layer=self.layer, details=self.details)
|
|
||||||
|
|
||||||
|
|
||||||
wkt = WKTElement(f'POINT({coord[0]} {coord[1]})', srid=4326)
|
wkt = WKTElement(f'POINT({coord[0]} {coord[1]})', srid=4326)
|
||||||
@@ -539,6 +548,6 @@ class ReverseGeocoder:
|
|||||||
result.distance = row.distance
|
result.distance = row.distance
|
||||||
if hasattr(row, 'bbox'):
|
if hasattr(row, 'bbox'):
|
||||||
result.bbox = Bbox.from_wkb(row.bbox.data)
|
result.bbox = Bbox.from_wkb(row.bbox.data)
|
||||||
await nres.add_result_details(self.conn, result, self.details)
|
await nres.add_result_details(self.conn, result, self.params)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -7,11 +7,13 @@
|
|||||||
"""
|
"""
|
||||||
Complex datatypes used by the Nominatim API.
|
Complex datatypes used by the Nominatim API.
|
||||||
"""
|
"""
|
||||||
from typing import Optional, Union, Tuple, NamedTuple
|
from typing import Optional, Union, Tuple, NamedTuple, TypeVar, Type, Dict, Any
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import enum
|
import enum
|
||||||
from struct import unpack
|
from struct import unpack
|
||||||
|
|
||||||
|
from nominatim.errors import UsageError
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class PlaceID:
|
class PlaceID:
|
||||||
""" Reference an object by Nominatim's internal ID.
|
""" Reference an object by Nominatim's internal ID.
|
||||||
@@ -164,10 +166,22 @@ class GeometryFormat(enum.Flag):
|
|||||||
TEXT = enum.auto()
|
TEXT = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
|
class DataLayer(enum.Flag):
|
||||||
|
""" Layer types that can be selected for reverse and forward search.
|
||||||
|
"""
|
||||||
|
POI = enum.auto()
|
||||||
|
ADDRESS = enum.auto()
|
||||||
|
RAILWAY = enum.auto()
|
||||||
|
MANMADE = enum.auto()
|
||||||
|
NATURAL = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
|
TParam = TypeVar('TParam', bound='LookupDetails') # pylint: disable=invalid-name
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class LookupDetails:
|
class LookupDetails:
|
||||||
""" Collection of parameters that define the amount of details
|
""" Collection of parameters that define the amount of details
|
||||||
returned with a search result.
|
returned with a lookup or details result.
|
||||||
"""
|
"""
|
||||||
geometry_output: GeometryFormat = GeometryFormat.NONE
|
geometry_output: GeometryFormat = GeometryFormat.NONE
|
||||||
""" Add the full geometry of the place to the result. Multiple
|
""" Add the full geometry of the place to the result. Multiple
|
||||||
@@ -194,12 +208,39 @@ class LookupDetails:
|
|||||||
more the geometry gets simplified.
|
more the geometry gets simplified.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_kwargs(cls: Type[TParam], kwargs: Dict[str, Any]) -> TParam:
|
||||||
|
""" Load the data fields of the class from a dictionary.
|
||||||
|
Unknown entries in the dictionary are ignored, missing ones
|
||||||
|
get the default setting.
|
||||||
|
|
||||||
class DataLayer(enum.Flag):
|
The function supports type checking and throws a UsageError
|
||||||
""" Layer types that can be selected for reverse and forward search.
|
when the value does not fit.
|
||||||
|
"""
|
||||||
|
def _check_field(v: Any, field: 'dataclasses.Field[Any]') -> Any:
|
||||||
|
if v is None:
|
||||||
|
return field.default_factory() \
|
||||||
|
if field.default_factory != dataclasses.MISSING \
|
||||||
|
else field.default
|
||||||
|
if field.metadata and 'transform' in field.metadata:
|
||||||
|
return field.metadata['transform'](v)
|
||||||
|
if not isinstance(v, field.type):
|
||||||
|
raise UsageError(f"Parameter '{field.name}' needs to be of {field.type!s}.")
|
||||||
|
return v
|
||||||
|
|
||||||
|
return cls(**{f.name: _check_field(kwargs[f.name], f)
|
||||||
|
for f in dataclasses.fields(cls) if f.name in kwargs})
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class ReverseDetails(LookupDetails):
|
||||||
|
""" Collection of parameters for the reverse call.
|
||||||
|
"""
|
||||||
|
max_rank: int = dataclasses.field(default=30,
|
||||||
|
metadata={'transform': lambda v: max(0, min(v, 30))}
|
||||||
|
)
|
||||||
|
""" Highest address rank to return.
|
||||||
|
"""
|
||||||
|
layers: DataLayer = DataLayer.ADDRESS | DataLayer.POI
|
||||||
|
""" Filter which kind of data to include.
|
||||||
"""
|
"""
|
||||||
POI = enum.auto()
|
|
||||||
ADDRESS = enum.auto()
|
|
||||||
RAILWAY = enum.auto()
|
|
||||||
MANMADE = enum.auto()
|
|
||||||
NATURAL = enum.auto()
|
|
||||||
|
|||||||
31
nominatim/api/v1/helpers.py
Normal file
31
nominatim/api/v1/helpers.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# 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.
|
||||||
|
"""
|
||||||
|
Helper function for parsing parameters and and outputting data
|
||||||
|
specifically for the v1 version of the API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
REVERSE_MAX_RANKS = [2, 2, 2, # 0-2 Continent/Sea
|
||||||
|
4, 4, # 3-4 Country
|
||||||
|
8, # 5 State
|
||||||
|
10, 10, # 6-7 Region
|
||||||
|
12, 12, # 8-9 County
|
||||||
|
16, 17, # 10-11 City
|
||||||
|
18, # 12 Town
|
||||||
|
19, # 13 Village/Suburb
|
||||||
|
22, # 14 Hamlet/Neighbourhood
|
||||||
|
25, # 15 Localities
|
||||||
|
26, # 16 Major Streets
|
||||||
|
27, # 17 Minor Streets
|
||||||
|
30 # 18 Building
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def zoom_to_rank(zoom: int) -> int:
|
||||||
|
""" Convert a zoom parameter into a rank according to the v1 API spec.
|
||||||
|
"""
|
||||||
|
return REVERSE_MAX_RANKS[max(0, min(18, zoom))]
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
Generic part of the server implementation of the v1 API.
|
Generic part of the server implementation of the v1 API.
|
||||||
Combine with the scaffolding provided for the various Python ASGI frameworks.
|
Combine with the scaffolding provided for the various Python ASGI frameworks.
|
||||||
"""
|
"""
|
||||||
from typing import Optional, Any, Type, Callable, NoReturn, cast
|
from typing import Optional, Any, Type, Callable, NoReturn, Dict, cast
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
import abc
|
import abc
|
||||||
import math
|
import math
|
||||||
@@ -17,6 +17,7 @@ from nominatim.config import Configuration
|
|||||||
import nominatim.api as napi
|
import nominatim.api as napi
|
||||||
import nominatim.api.logging as loglib
|
import nominatim.api.logging as loglib
|
||||||
from nominatim.api.v1.format import dispatch as formatting
|
from nominatim.api.v1.format import dispatch as formatting
|
||||||
|
from nominatim.api.v1 import helpers
|
||||||
|
|
||||||
CONTENT_TYPE = {
|
CONTENT_TYPE = {
|
||||||
'text': 'text/plain; charset=utf-8',
|
'text': 'text/plain; charset=utf-8',
|
||||||
@@ -226,31 +227,32 @@ class ASGIAdaptor(abc.ABC):
|
|||||||
return fmt
|
return fmt
|
||||||
|
|
||||||
|
|
||||||
def parse_geometry_details(self, fmt: str) -> napi.LookupDetails:
|
def parse_geometry_details(self, fmt: str) -> Dict[str, Any]:
|
||||||
""" Create details strucutre from the supplied geometry parameters.
|
""" 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
|
numgeoms = 0
|
||||||
|
output = napi.GeometryFormat.NONE
|
||||||
if self.get_bool('polygon_geojson', False):
|
if self.get_bool('polygon_geojson', False):
|
||||||
details.geometry_output |= napi.GeometryFormat.GEOJSON
|
output |= napi.GeometryFormat.GEOJSON
|
||||||
numgeoms += 1
|
numgeoms += 1
|
||||||
if fmt not in ('geojson', 'geocodejson'):
|
if fmt not in ('geojson', 'geocodejson'):
|
||||||
if self.get_bool('polygon_text', False):
|
if self.get_bool('polygon_text', False):
|
||||||
details.geometry_output |= napi.GeometryFormat.TEXT
|
output |= napi.GeometryFormat.TEXT
|
||||||
numgeoms += 1
|
numgeoms += 1
|
||||||
if self.get_bool('polygon_kml', False):
|
if self.get_bool('polygon_kml', False):
|
||||||
details.geometry_output |= napi.GeometryFormat.KML
|
output |= napi.GeometryFormat.KML
|
||||||
numgeoms += 1
|
numgeoms += 1
|
||||||
if self.get_bool('polygon_svg', False):
|
if self.get_bool('polygon_svg', False):
|
||||||
details.geometry_output |= napi.GeometryFormat.SVG
|
output |= napi.GeometryFormat.SVG
|
||||||
numgeoms += 1
|
numgeoms += 1
|
||||||
|
|
||||||
if numgeoms > self.config().get_int('POLYGON_OUTPUT_MAX_TYPES'):
|
if numgeoms > self.config().get_int('POLYGON_OUTPUT_MAX_TYPES'):
|
||||||
self.raise_error('Too many polgyon output options selected.')
|
self.raise_error('Too many polgyon output options selected.')
|
||||||
|
|
||||||
return details
|
return {'address_details': True,
|
||||||
|
'geometry_simplification': self.get_float('polygon_threshold', 0.0),
|
||||||
|
'geometry_output': output
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def status_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) -> Any:
|
async def status_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) -> Any:
|
||||||
@@ -285,17 +287,17 @@ async def details_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) ->
|
|||||||
|
|
||||||
debug = params.setup_debugging()
|
debug = params.setup_debugging()
|
||||||
|
|
||||||
details = napi.LookupDetails(address_details=params.get_bool('addressdetails', False),
|
|
||||||
linked_places=params.get_bool('linkedplaces', False),
|
|
||||||
parented_places=params.get_bool('hierarchy', False),
|
|
||||||
keywords=params.get_bool('keywords', False))
|
|
||||||
|
|
||||||
if params.get_bool('polygon_geojson', False):
|
|
||||||
details.geometry_output = napi.GeometryFormat.GEOJSON
|
|
||||||
|
|
||||||
locales = napi.Locales.from_accept_languages(params.get_accepted_languages())
|
locales = napi.Locales.from_accept_languages(params.get_accepted_languages())
|
||||||
|
|
||||||
result = await api.details(place, details)
|
result = await api.details(place,
|
||||||
|
address_details=params.get_bool('addressdetails', False),
|
||||||
|
linked_places=params.get_bool('linkedplaces', False),
|
||||||
|
parented_places=params.get_bool('hierarchy', False),
|
||||||
|
keywords=params.get_bool('keywords', False),
|
||||||
|
geometry_output = napi.GeometryFormat.GEOJSON
|
||||||
|
if params.get_bool('polygon_geojson', False)
|
||||||
|
else napi.GeometryFormat.NONE
|
||||||
|
)
|
||||||
|
|
||||||
if debug:
|
if debug:
|
||||||
return params.build_response(loglib.get_and_disable())
|
return params.build_response(loglib.get_and_disable())
|
||||||
@@ -318,15 +320,12 @@ async def reverse_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) ->
|
|||||||
debug = params.setup_debugging()
|
debug = params.setup_debugging()
|
||||||
coord = napi.Point(params.get_float('lon'), params.get_float('lat'))
|
coord = napi.Point(params.get_float('lon'), params.get_float('lat'))
|
||||||
locales = napi.Locales.from_accept_languages(params.get_accepted_languages())
|
locales = napi.Locales.from_accept_languages(params.get_accepted_languages())
|
||||||
|
|
||||||
details = params.parse_geometry_details(fmt)
|
details = params.parse_geometry_details(fmt)
|
||||||
|
details['max_rank'] = helpers.zoom_to_rank(params.get_int('zoom', 18))
|
||||||
|
details['layers'] = params.get_layers()
|
||||||
|
|
||||||
zoom = max(0, min(18, params.get_int('zoom', 18)))
|
result = await api.reverse(coord, **details)
|
||||||
|
|
||||||
|
|
||||||
result = await api.reverse(coord, REVERSE_MAX_RANKS[zoom],
|
|
||||||
params.get_layers() or
|
|
||||||
napi.DataLayer.ADDRESS | napi.DataLayer.POI,
|
|
||||||
details)
|
|
||||||
|
|
||||||
if debug:
|
if debug:
|
||||||
return params.build_response(loglib.get_and_disable())
|
return params.build_response(loglib.get_and_disable())
|
||||||
@@ -357,7 +356,7 @@ async def lookup_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) -> A
|
|||||||
places.append(napi.OsmID(oid[0], int(oid[1:])))
|
places.append(napi.OsmID(oid[0], int(oid[1:])))
|
||||||
|
|
||||||
if places:
|
if places:
|
||||||
results = await api.lookup(places, details)
|
results = await api.lookup(places, **details)
|
||||||
else:
|
else:
|
||||||
results = napi.SearchResults()
|
results = napi.SearchResults()
|
||||||
|
|
||||||
@@ -375,22 +374,6 @@ async def lookup_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) -> A
|
|||||||
|
|
||||||
EndpointFunc = Callable[[napi.NominatimAPIAsync, ASGIAdaptor], Any]
|
EndpointFunc = Callable[[napi.NominatimAPIAsync, ASGIAdaptor], Any]
|
||||||
|
|
||||||
REVERSE_MAX_RANKS = [2, 2, 2, # 0-2 Continent/Sea
|
|
||||||
4, 4, # 3-4 Country
|
|
||||||
8, # 5 State
|
|
||||||
10, 10, # 6-7 Region
|
|
||||||
12, 12, # 8-9 County
|
|
||||||
16, 17, # 10-11 City
|
|
||||||
18, # 12 Town
|
|
||||||
19, # 13 Village/Suburb
|
|
||||||
22, # 14 Hamlet/Neighbourhood
|
|
||||||
25, # 15 Localities
|
|
||||||
26, # 16 Major Streets
|
|
||||||
27, # 17 Minor Streets
|
|
||||||
30 # 18 Building
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
ROUTES = [
|
ROUTES = [
|
||||||
('status', status_endpoint),
|
('status', status_endpoint),
|
||||||
('details', details_endpoint),
|
('details', details_endpoint),
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from nominatim.errors import UsageError
|
|||||||
from nominatim.clicmd.args import NominatimArgs
|
from nominatim.clicmd.args import NominatimArgs
|
||||||
import nominatim.api as napi
|
import nominatim.api as napi
|
||||||
import nominatim.api.v1 as api_output
|
import nominatim.api.v1 as api_output
|
||||||
from nominatim.api.v1.server_glue import REVERSE_MAX_RANKS
|
from nominatim.api.v1.helpers import zoom_to_rank
|
||||||
|
|
||||||
# Do not repeat documentation of subcommand classes.
|
# Do not repeat documentation of subcommand classes.
|
||||||
# pylint: disable=C0111
|
# pylint: disable=C0111
|
||||||
@@ -163,14 +163,12 @@ class APIReverse:
|
|||||||
def run(self, args: NominatimArgs) -> int:
|
def run(self, args: NominatimArgs) -> int:
|
||||||
api = napi.NominatimAPI(args.project_dir)
|
api = napi.NominatimAPI(args.project_dir)
|
||||||
|
|
||||||
details = napi.LookupDetails(address_details=True, # needed for display name
|
|
||||||
geometry_output=args.get_geometry_output(),
|
|
||||||
geometry_simplification=args.polygon_threshold or 0.0)
|
|
||||||
|
|
||||||
result = api.reverse(napi.Point(args.lon, args.lat),
|
result = api.reverse(napi.Point(args.lon, args.lat),
|
||||||
REVERSE_MAX_RANKS[max(0, min(18, args.zoom or 18))],
|
max_rank=zoom_to_rank(args.zoom or 18),
|
||||||
args.get_layers(napi.DataLayer.ADDRESS | napi.DataLayer.POI),
|
layers=args.get_layers(napi.DataLayer.ADDRESS | napi.DataLayer.POI),
|
||||||
details)
|
address_details=True, # needed for display name
|
||||||
|
geometry_output=args.get_geometry_output(),
|
||||||
|
geometry_simplification=args.polygon_threshold)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
output = api_output.format_result(
|
output = api_output.format_result(
|
||||||
@@ -216,13 +214,12 @@ class APILookup:
|
|||||||
def run(self, args: NominatimArgs) -> int:
|
def run(self, args: NominatimArgs) -> int:
|
||||||
api = napi.NominatimAPI(args.project_dir)
|
api = napi.NominatimAPI(args.project_dir)
|
||||||
|
|
||||||
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]
|
places = [napi.OsmID(o[0], int(o[1:])) for o in args.ids]
|
||||||
|
|
||||||
results = api.lookup(places, details)
|
results = api.lookup(places,
|
||||||
|
address_details=True, # needed for display name
|
||||||
|
geometry_output=args.get_geometry_output(),
|
||||||
|
geometry_simplification=args.polygon_threshold or 0.0)
|
||||||
|
|
||||||
output = api_output.format_result(
|
output = api_output.format_result(
|
||||||
results,
|
results,
|
||||||
@@ -297,14 +294,15 @@ class APIDetails:
|
|||||||
|
|
||||||
api = napi.NominatimAPI(args.project_dir)
|
api = napi.NominatimAPI(args.project_dir)
|
||||||
|
|
||||||
details = napi.LookupDetails(address_details=args.addressdetails,
|
result = api.details(place,
|
||||||
linked_places=args.linkedplaces,
|
address_details=args.addressdetails,
|
||||||
parented_places=args.hierarchy,
|
linked_places=args.linkedplaces,
|
||||||
keywords=args.keywords)
|
parented_places=args.hierarchy,
|
||||||
if args.polygon_geojson:
|
keywords=args.keywords,
|
||||||
details.geometry_output = napi.GeometryFormat.GEOJSON
|
geometry_output=napi.GeometryFormat.GEOJSON
|
||||||
|
if args.polygon_geojson
|
||||||
|
else napi.GeometryFormat.NONE)
|
||||||
|
|
||||||
result = api.details(place, details)
|
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
output = api_output.format_result(
|
output = api_output.format_result(
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ def test_lookup_in_placex(apiobj, idobj):
|
|||||||
indexed_date=import_date,
|
indexed_date=import_date,
|
||||||
geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)')
|
geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)')
|
||||||
|
|
||||||
result = apiobj.api.details(idobj, napi.LookupDetails())
|
result = apiobj.api.details(idobj)
|
||||||
|
|
||||||
assert result is not None
|
assert result is not None
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ def test_lookup_in_placex_minimal_info(apiobj):
|
|||||||
indexed_date=import_date,
|
indexed_date=import_date,
|
||||||
geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)')
|
geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)')
|
||||||
|
|
||||||
result = apiobj.api.details(napi.PlaceID(332), napi.LookupDetails())
|
result = apiobj.api.details(napi.PlaceID(332))
|
||||||
|
|
||||||
assert result is not None
|
assert result is not None
|
||||||
|
|
||||||
@@ -121,8 +121,7 @@ def test_lookup_in_placex_with_geometry(apiobj):
|
|||||||
apiobj.add_placex(place_id=332,
|
apiobj.add_placex(place_id=332,
|
||||||
geometry='LINESTRING(23 34, 23.1 34)')
|
geometry='LINESTRING(23 34, 23.1 34)')
|
||||||
|
|
||||||
result = apiobj.api.details(napi.PlaceID(332),
|
result = apiobj.api.details(napi.PlaceID(332), geometry_output=napi.GeometryFormat.GEOJSON)
|
||||||
napi.LookupDetails(geometry_output=napi.GeometryFormat.GEOJSON))
|
|
||||||
|
|
||||||
assert result.geometry == {'geojson': '{"type":"LineString","coordinates":[[23,34],[23.1,34]]}'}
|
assert result.geometry == {'geojson': '{"type":"LineString","coordinates":[[23,34],[23.1,34]]}'}
|
||||||
|
|
||||||
@@ -144,8 +143,7 @@ def test_lookup_placex_with_address_details(apiobj):
|
|||||||
country_code='pl',
|
country_code='pl',
|
||||||
rank_search=17, rank_address=16)
|
rank_search=17, rank_address=16)
|
||||||
|
|
||||||
result = apiobj.api.details(napi.PlaceID(332),
|
result = apiobj.api.details(napi.PlaceID(332), address_details=True)
|
||||||
napi.LookupDetails(address_details=True))
|
|
||||||
|
|
||||||
assert result.address_rows == [
|
assert result.address_rows == [
|
||||||
napi.AddressLine(place_id=332, osm_object=('W', 4),
|
napi.AddressLine(place_id=332, osm_object=('W', 4),
|
||||||
@@ -177,8 +175,7 @@ def test_lookup_place_with_linked_places_none_existing(apiobj):
|
|||||||
country_code='pl', linked_place_id=45,
|
country_code='pl', linked_place_id=45,
|
||||||
rank_search=27, rank_address=26)
|
rank_search=27, rank_address=26)
|
||||||
|
|
||||||
result = apiobj.api.details(napi.PlaceID(332),
|
result = apiobj.api.details(napi.PlaceID(332), linked_places=True)
|
||||||
napi.LookupDetails(linked_places=True))
|
|
||||||
|
|
||||||
assert result.linked_rows == []
|
assert result.linked_rows == []
|
||||||
|
|
||||||
@@ -197,8 +194,7 @@ def test_lookup_place_with_linked_places_existing(apiobj):
|
|||||||
country_code='pl', linked_place_id=332,
|
country_code='pl', linked_place_id=332,
|
||||||
rank_search=27, rank_address=26)
|
rank_search=27, rank_address=26)
|
||||||
|
|
||||||
result = apiobj.api.details(napi.PlaceID(332),
|
result = apiobj.api.details(napi.PlaceID(332), linked_places=True)
|
||||||
napi.LookupDetails(linked_places=True))
|
|
||||||
|
|
||||||
assert result.linked_rows == [
|
assert result.linked_rows == [
|
||||||
napi.AddressLine(place_id=1001, osm_object=('W', 5),
|
napi.AddressLine(place_id=1001, osm_object=('W', 5),
|
||||||
@@ -220,8 +216,7 @@ def test_lookup_place_with_parented_places_not_existing(apiobj):
|
|||||||
country_code='pl', parent_place_id=45,
|
country_code='pl', parent_place_id=45,
|
||||||
rank_search=27, rank_address=26)
|
rank_search=27, rank_address=26)
|
||||||
|
|
||||||
result = apiobj.api.details(napi.PlaceID(332),
|
result = apiobj.api.details(napi.PlaceID(332), parented_places=True)
|
||||||
napi.LookupDetails(parented_places=True))
|
|
||||||
|
|
||||||
assert result.parented_rows == []
|
assert result.parented_rows == []
|
||||||
|
|
||||||
@@ -240,8 +235,7 @@ def test_lookup_place_with_parented_places_existing(apiobj):
|
|||||||
country_code='pl', parent_place_id=332,
|
country_code='pl', parent_place_id=332,
|
||||||
rank_search=27, rank_address=26)
|
rank_search=27, rank_address=26)
|
||||||
|
|
||||||
result = apiobj.api.details(napi.PlaceID(332),
|
result = apiobj.api.details(napi.PlaceID(332), parented_places=True)
|
||||||
napi.LookupDetails(parented_places=True))
|
|
||||||
|
|
||||||
assert result.parented_rows == [
|
assert result.parented_rows == [
|
||||||
napi.AddressLine(place_id=1001, osm_object=('N', 5),
|
napi.AddressLine(place_id=1001, osm_object=('N', 5),
|
||||||
@@ -263,7 +257,7 @@ def test_lookup_in_osmline(apiobj, idobj):
|
|||||||
indexed_date=import_date,
|
indexed_date=import_date,
|
||||||
geometry='LINESTRING(23 34, 23 35)')
|
geometry='LINESTRING(23 34, 23 35)')
|
||||||
|
|
||||||
result = apiobj.api.details(idobj, napi.LookupDetails())
|
result = apiobj.api.details(idobj)
|
||||||
|
|
||||||
assert result is not None
|
assert result is not None
|
||||||
|
|
||||||
@@ -310,13 +304,13 @@ def test_lookup_in_osmline_split_interpolation(apiobj):
|
|||||||
startnumber=11, endnumber=20, step=1)
|
startnumber=11, endnumber=20, step=1)
|
||||||
|
|
||||||
for i in range(1, 6):
|
for i in range(1, 6):
|
||||||
result = apiobj.api.details(napi.OsmID('W', 9, str(i)), napi.LookupDetails())
|
result = apiobj.api.details(napi.OsmID('W', 9, str(i)))
|
||||||
assert result.place_id == 1000
|
assert result.place_id == 1000
|
||||||
for i in range(7, 11):
|
for i in range(7, 11):
|
||||||
result = apiobj.api.details(napi.OsmID('W', 9, str(i)), napi.LookupDetails())
|
result = apiobj.api.details(napi.OsmID('W', 9, str(i)))
|
||||||
assert result.place_id == 1001
|
assert result.place_id == 1001
|
||||||
for i in range(12, 22):
|
for i in range(12, 22):
|
||||||
result = apiobj.api.details(napi.OsmID('W', 9, str(i)), napi.LookupDetails())
|
result = apiobj.api.details(napi.OsmID('W', 9, str(i)))
|
||||||
assert result.place_id == 1002
|
assert result.place_id == 1002
|
||||||
|
|
||||||
|
|
||||||
@@ -340,8 +334,7 @@ def test_lookup_osmline_with_address_details(apiobj):
|
|||||||
country_code='pl',
|
country_code='pl',
|
||||||
rank_search=17, rank_address=16)
|
rank_search=17, rank_address=16)
|
||||||
|
|
||||||
result = apiobj.api.details(napi.PlaceID(9000),
|
result = apiobj.api.details(napi.PlaceID(9000), address_details=True)
|
||||||
napi.LookupDetails(address_details=True))
|
|
||||||
|
|
||||||
assert result.address_rows == [
|
assert result.address_rows == [
|
||||||
napi.AddressLine(place_id=None, osm_object=None,
|
napi.AddressLine(place_id=None, osm_object=None,
|
||||||
@@ -383,7 +376,7 @@ def test_lookup_in_tiger(apiobj):
|
|||||||
osm_type='W', osm_id=6601223,
|
osm_type='W', osm_id=6601223,
|
||||||
geometry='LINESTRING(23 34, 23 35)')
|
geometry='LINESTRING(23 34, 23 35)')
|
||||||
|
|
||||||
result = apiobj.api.details(napi.PlaceID(4924), napi.LookupDetails())
|
result = apiobj.api.details(napi.PlaceID(4924))
|
||||||
|
|
||||||
assert result is not None
|
assert result is not None
|
||||||
|
|
||||||
@@ -441,8 +434,7 @@ def test_lookup_tiger_with_address_details(apiobj):
|
|||||||
country_code='us',
|
country_code='us',
|
||||||
rank_search=17, rank_address=16)
|
rank_search=17, rank_address=16)
|
||||||
|
|
||||||
result = apiobj.api.details(napi.PlaceID(9000),
|
result = apiobj.api.details(napi.PlaceID(9000), address_details=True)
|
||||||
napi.LookupDetails(address_details=True))
|
|
||||||
|
|
||||||
assert result.address_rows == [
|
assert result.address_rows == [
|
||||||
napi.AddressLine(place_id=None, osm_object=None,
|
napi.AddressLine(place_id=None, osm_object=None,
|
||||||
@@ -483,7 +475,7 @@ def test_lookup_in_postcode(apiobj):
|
|||||||
indexed_date=import_date,
|
indexed_date=import_date,
|
||||||
geometry='POINT(-9.45 5.6)')
|
geometry='POINT(-9.45 5.6)')
|
||||||
|
|
||||||
result = apiobj.api.details(napi.PlaceID(554), napi.LookupDetails())
|
result = apiobj.api.details(napi.PlaceID(554))
|
||||||
|
|
||||||
assert result is not None
|
assert result is not None
|
||||||
|
|
||||||
@@ -537,8 +529,7 @@ def test_lookup_postcode_with_address_details(apiobj):
|
|||||||
country_code='gb',
|
country_code='gb',
|
||||||
rank_search=17, rank_address=16)
|
rank_search=17, rank_address=16)
|
||||||
|
|
||||||
result = apiobj.api.details(napi.PlaceID(9000),
|
result = apiobj.api.details(napi.PlaceID(9000), address_details=True)
|
||||||
napi.LookupDetails(address_details=True))
|
|
||||||
|
|
||||||
assert result.address_rows == [
|
assert result.address_rows == [
|
||||||
napi.AddressLine(place_id=332, osm_object=('N', 3333),
|
napi.AddressLine(place_id=332, osm_object=('N', 3333),
|
||||||
@@ -570,7 +561,7 @@ def test_lookup_missing_object(apiobj, objid):
|
|||||||
apiobj.add_placex(place_id=1, osm_type='N', osm_id=55,
|
apiobj.add_placex(place_id=1, osm_type='N', osm_id=55,
|
||||||
class_='place', type='suburb')
|
class_='place', type='suburb')
|
||||||
|
|
||||||
assert apiobj.api.details(objid, napi.LookupDetails()) is None
|
assert apiobj.api.details(objid) is None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('gtype', (napi.GeometryFormat.KML,
|
@pytest.mark.parametrize('gtype', (napi.GeometryFormat.KML,
|
||||||
@@ -580,5 +571,4 @@ def test_lookup_unsupported_geometry(apiobj, gtype):
|
|||||||
apiobj.add_placex(place_id=332)
|
apiobj.add_placex(place_id=332)
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
apiobj.api.details(napi.PlaceID(332),
|
apiobj.api.details(napi.PlaceID(332), geometry_output=gtype)
|
||||||
napi.LookupDetails(geometry_output=gtype))
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ def test_lookup_multiple_places(apiobj):
|
|||||||
|
|
||||||
result = apiobj.api.lookup((napi.OsmID('W', 1),
|
result = apiobj.api.lookup((napi.OsmID('W', 1),
|
||||||
napi.OsmID('W', 4),
|
napi.OsmID('W', 4),
|
||||||
napi.OsmID('W', 9928)), napi.LookupDetails())
|
napi.OsmID('W', 9928)))
|
||||||
|
|
||||||
assert len(result) == 2
|
assert len(result) == 2
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ def test_reverse_rank_30_layers(apiobj, y, layer, place_id):
|
|||||||
rank_search=30,
|
rank_search=30,
|
||||||
centroid=(1.3, 0.70005))
|
centroid=(1.3, 0.70005))
|
||||||
|
|
||||||
assert apiobj.api.reverse((1.3, y), layer=layer).place_id == place_id
|
assert apiobj.api.reverse((1.3, y), layers=layer).place_id == place_id
|
||||||
|
|
||||||
|
|
||||||
def test_reverse_poi_layer_with_no_pois(apiobj):
|
def test_reverse_poi_layer_with_no_pois(apiobj):
|
||||||
@@ -95,7 +95,7 @@ def test_reverse_poi_layer_with_no_pois(apiobj):
|
|||||||
centroid=(1.3, 0.70001))
|
centroid=(1.3, 0.70001))
|
||||||
|
|
||||||
assert apiobj.api.reverse((1.3, 0.70001), max_rank=29,
|
assert apiobj.api.reverse((1.3, 0.70001), max_rank=29,
|
||||||
layer=napi.DataLayer.POI) is None
|
layers=napi.DataLayer.POI) is None
|
||||||
|
|
||||||
|
|
||||||
def test_reverse_housenumber_on_street(apiobj):
|
def test_reverse_housenumber_on_street(apiobj):
|
||||||
@@ -245,7 +245,7 @@ def test_reverse_larger_area_layers(apiobj, layer, place_id):
|
|||||||
rank_search=16,
|
rank_search=16,
|
||||||
centroid=(1.3, 0.70005))
|
centroid=(1.3, 0.70005))
|
||||||
|
|
||||||
assert apiobj.api.reverse((1.3, 0.7), layer=layer).place_id == place_id
|
assert apiobj.api.reverse((1.3, 0.7), layers=layer).place_id == place_id
|
||||||
|
|
||||||
|
|
||||||
def test_reverse_country_lookup_no_objects(apiobj):
|
def test_reverse_country_lookup_no_objects(apiobj):
|
||||||
@@ -296,10 +296,8 @@ def test_reverse_geometry_output_placex(apiobj, gtype):
|
|||||||
country_code='xx',
|
country_code='xx',
|
||||||
centroid=(0.5, 0.5))
|
centroid=(0.5, 0.5))
|
||||||
|
|
||||||
details = napi.LookupDetails(geometry_output=gtype)
|
assert apiobj.api.reverse((59.3, 80.70001), geometry_output=gtype).place_id == 1001
|
||||||
|
assert apiobj.api.reverse((0.5, 0.5), geometry_output=gtype).place_id == 1003
|
||||||
assert apiobj.api.reverse((59.3, 80.70001), details=details).place_id == 1001
|
|
||||||
assert apiobj.api.reverse((0.5, 0.5), details=details).place_id == 1003
|
|
||||||
|
|
||||||
|
|
||||||
def test_reverse_simplified_geometry(apiobj):
|
def test_reverse_simplified_geometry(apiobj):
|
||||||
@@ -309,9 +307,9 @@ def test_reverse_simplified_geometry(apiobj):
|
|||||||
rank_search=30,
|
rank_search=30,
|
||||||
centroid=(59.3, 80.70001))
|
centroid=(59.3, 80.70001))
|
||||||
|
|
||||||
details = napi.LookupDetails(geometry_output=napi.GeometryFormat.GEOJSON,
|
details = dict(geometry_output=napi.GeometryFormat.GEOJSON,
|
||||||
geometry_simplification=0.1)
|
geometry_simplification=0.1)
|
||||||
assert apiobj.api.reverse((59.3, 80.70001), details=details).place_id == 1001
|
assert apiobj.api.reverse((59.3, 80.70001), **details).place_id == 1001
|
||||||
|
|
||||||
|
|
||||||
def test_reverse_interpolation_geometry(apiobj):
|
def test_reverse_interpolation_geometry(apiobj):
|
||||||
@@ -321,8 +319,7 @@ def test_reverse_interpolation_geometry(apiobj):
|
|||||||
centroid=(10.0, 10.00001),
|
centroid=(10.0, 10.00001),
|
||||||
geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
|
geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
|
||||||
|
|
||||||
details = napi.LookupDetails(geometry_output=napi.GeometryFormat.TEXT)
|
assert apiobj.api.reverse((10.0, 10.0), geometry_output=napi.GeometryFormat.TEXT)\
|
||||||
assert apiobj.api.reverse((10.0, 10.0), details=details)\
|
|
||||||
.geometry['text'] == 'POINT(10 10.00001)'
|
.geometry['text'] == 'POINT(10 10.00001)'
|
||||||
|
|
||||||
|
|
||||||
@@ -339,8 +336,8 @@ def test_reverse_tiger_geometry(apiobj):
|
|||||||
centroid=(10.0, 10.00001),
|
centroid=(10.0, 10.00001),
|
||||||
geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
|
geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
|
||||||
|
|
||||||
details = napi.LookupDetails(geometry_output=napi.GeometryFormat.GEOJSON)
|
output = apiobj.api.reverse((10.0, 10.0),
|
||||||
output = apiobj.api.reverse((10.0, 10.0), details=details).geometry['geojson']
|
geometry_output=napi.GeometryFormat.GEOJSON).geometry['geojson']
|
||||||
|
|
||||||
assert json.loads(output) == {'coordinates': [10, 10.00001], 'type': 'Point'}
|
assert json.loads(output) == {'coordinates': [10, 10.00001], 'type': 'Point'}
|
||||||
|
|
||||||
|
|||||||
35
test/python/api/test_api_types.py
Normal file
35
test/python/api/test_api_types.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# 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 loading of parameter dataclasses.
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from nominatim.errors import UsageError
|
||||||
|
import nominatim.api.types as typ
|
||||||
|
|
||||||
|
def test_no_params_defaults():
|
||||||
|
params = typ.LookupDetails.from_kwargs({})
|
||||||
|
|
||||||
|
assert not params.parented_places
|
||||||
|
assert params.geometry_simplification == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('k,v', [('geometry_output', 'a'),
|
||||||
|
('linked_places', 0),
|
||||||
|
('geometry_simplification', 'NaN')])
|
||||||
|
def test_bad_format_reverse(k, v):
|
||||||
|
with pytest.raises(UsageError):
|
||||||
|
params = typ.ReverseDetails.from_kwargs({k: v})
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('rin,rout', [(-23, 0), (0, 0), (1, 1),
|
||||||
|
(15, 15), (30, 30), (31, 30)])
|
||||||
|
def test_rank_params(rin, rout):
|
||||||
|
params = typ.ReverseDetails.from_kwargs({'max_rank': rin})
|
||||||
|
|
||||||
|
assert params.max_rank == rout
|
||||||
@@ -81,7 +81,7 @@ class TestCliDetailsCall:
|
|||||||
napi.Point(1.0, -3.0))
|
napi.Point(1.0, -3.0))
|
||||||
|
|
||||||
monkeypatch.setattr(napi.NominatimAPI, 'details',
|
monkeypatch.setattr(napi.NominatimAPI, 'details',
|
||||||
lambda *args: result)
|
lambda *args, **kwargs: result)
|
||||||
|
|
||||||
@pytest.mark.parametrize("params", [('--node', '1'),
|
@pytest.mark.parametrize("params", [('--node', '1'),
|
||||||
('--way', '1'),
|
('--way', '1'),
|
||||||
@@ -106,7 +106,7 @@ class TestCliReverseCall:
|
|||||||
extratags={'extra':'Extra'})
|
extratags={'extra':'Extra'})
|
||||||
|
|
||||||
monkeypatch.setattr(napi.NominatimAPI, 'reverse',
|
monkeypatch.setattr(napi.NominatimAPI, 'reverse',
|
||||||
lambda *args: result)
|
lambda *args, **kwargs: result)
|
||||||
|
|
||||||
|
|
||||||
def test_reverse_simple(self, cli_call, tmp_path, capsys):
|
def test_reverse_simple(self, cli_call, tmp_path, capsys):
|
||||||
@@ -165,7 +165,7 @@ class TestCliLookupCall:
|
|||||||
extratags={'extra':'Extra'})
|
extratags={'extra':'Extra'})
|
||||||
|
|
||||||
monkeypatch.setattr(napi.NominatimAPI, 'lookup',
|
monkeypatch.setattr(napi.NominatimAPI, 'lookup',
|
||||||
lambda *args: napi.SearchResults([result]))
|
lambda *args, **kwargs: napi.SearchResults([result]))
|
||||||
|
|
||||||
def test_lookup_simple(self, cli_call, tmp_path, capsys):
|
def test_lookup_simple(self, cli_call, tmp_path, capsys):
|
||||||
result = cli_call('lookup', '--project-dir', str(tmp_path),
|
result = cli_call('lookup', '--project-dir', str(tmp_path),
|
||||||
|
|||||||
Reference in New Issue
Block a user