mirror of
https://github.com/osm-search/Nominatim.git
synced 2026-02-26 11:08:13 +00:00
make sure PHP and Python reverse code does the same
The only allowable difference is precision of coordinates. Python uses a precision of 7 digits where possible, which corresponds to the precision of OSM data. Also fixes some smaller bugs found by the BDD tests.
This commit is contained in:
@@ -102,14 +102,17 @@ async def find_in_tiger(conn: SearchConnection, place: ntyp.PlaceRef,
|
|||||||
"""
|
"""
|
||||||
log().section("Find in TIGER table")
|
log().section("Find in TIGER table")
|
||||||
t = conn.t.tiger
|
t = conn.t.tiger
|
||||||
|
parent = conn.t.placex
|
||||||
sql = sa.select(t.c.place_id, t.c.parent_place_id,
|
sql = sa.select(t.c.place_id, t.c.parent_place_id,
|
||||||
|
parent.c.osm_type, parent.c.osm_id,
|
||||||
t.c.startnumber, t.c.endnumber, t.c.step,
|
t.c.startnumber, t.c.endnumber, t.c.step,
|
||||||
t.c.postcode,
|
t.c.postcode,
|
||||||
t.c.linegeo.ST_Centroid().label('centroid'),
|
t.c.linegeo.ST_Centroid().label('centroid'),
|
||||||
_select_column_geometry(t.c.linegeo, details.geometry_output))
|
_select_column_geometry(t.c.linegeo, details.geometry_output))
|
||||||
|
|
||||||
if isinstance(place, ntyp.PlaceID):
|
if isinstance(place, ntyp.PlaceID):
|
||||||
sql = sql.where(t.c.place_id == place.place_id)
|
sql = sql.where(t.c.place_id == place.place_id)\
|
||||||
|
.join(parent, t.c.parent_place_id == parent.c.place_id, isouter=True)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -255,6 +255,7 @@ def create_from_tiger_row(row: Optional[SaRow],
|
|||||||
|
|
||||||
res = class_type(source_table=SourceTable.TIGER,
|
res = class_type(source_table=SourceTable.TIGER,
|
||||||
place_id=row.place_id,
|
place_id=row.place_id,
|
||||||
|
osm_object=(row.osm_type, row.osm_id),
|
||||||
category=('place', 'houses' if hnr is None else 'house'),
|
category=('place', 'houses' if hnr is None else 'house'),
|
||||||
postcode=row.postcode,
|
postcode=row.postcode,
|
||||||
country_code='us',
|
country_code='us',
|
||||||
@@ -315,8 +316,8 @@ def _result_row_to_address_row(row: SaRow) -> AddressLine:
|
|||||||
""" Create a new AddressLine from the results of a datbase query.
|
""" Create a new AddressLine from the results of a datbase query.
|
||||||
"""
|
"""
|
||||||
extratags: Dict[str, str] = getattr(row, 'extratags', {})
|
extratags: Dict[str, str] = getattr(row, 'extratags', {})
|
||||||
if 'place_type' in row:
|
if hasattr(row, 'place_type') and row.place_type:
|
||||||
extratags['place_type'] = row.place_type
|
extratags['place'] = row.place_type
|
||||||
|
|
||||||
names = row.name
|
names = row.name
|
||||||
if getattr(row, 'housenumber', None) is not None:
|
if getattr(row, 'housenumber', None) is not None:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"""
|
"""
|
||||||
Implementation of reverse geocoding.
|
Implementation of reverse geocoding.
|
||||||
"""
|
"""
|
||||||
from typing import Optional, List
|
from typing import Optional, List, Callable, Type, Tuple
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from geoalchemy2 import WKTElement
|
from geoalchemy2 import WKTElement
|
||||||
@@ -16,12 +16,14 @@ 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
|
from nominatim.api.types import AnyPoint, DataLayer, LookupDetails, 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.
|
||||||
# pylint: disable=singleton-comparison
|
# pylint: disable=singleton-comparison
|
||||||
|
|
||||||
|
RowFunc = Callable[[Optional[SaRow], Type[nres.ReverseResult]], Optional[nres.ReverseResult]]
|
||||||
|
|
||||||
def _select_from_placex(t: SaFromClause, wkt: Optional[str] = None) -> SaSelect:
|
def _select_from_placex(t: SaFromClause, wkt: Optional[str] = None) -> SaSelect:
|
||||||
""" Create a select statement with the columns relevant for reverse
|
""" Create a select statement with the columns relevant for reverse
|
||||||
results.
|
results.
|
||||||
@@ -37,7 +39,11 @@ def _select_from_placex(t: SaFromClause, wkt: Optional[str] = None) -> SaSelect:
|
|||||||
t.c.housenumber, t.c.postcode, t.c.country_code,
|
t.c.housenumber, t.c.postcode, t.c.country_code,
|
||||||
t.c.importance, t.c.wikipedia,
|
t.c.importance, t.c.wikipedia,
|
||||||
t.c.parent_place_id, t.c.rank_address, t.c.rank_search,
|
t.c.parent_place_id, t.c.rank_address, t.c.rank_search,
|
||||||
t.c.centroid,
|
sa.case(
|
||||||
|
(t.c.geometry.ST_GeometryType().in_(('ST_LineString',
|
||||||
|
'ST_MultiLineString')),
|
||||||
|
t.c.geometry.ST_ClosestPoint(wkt)),
|
||||||
|
else_=t.c.centroid).label('centroid'),
|
||||||
distance.label('distance'),
|
distance.label('distance'),
|
||||||
t.c.geometry.ST_Expand(0).label('bbox'))
|
t.c.geometry.ST_Expand(0).label('bbox'))
|
||||||
|
|
||||||
@@ -49,6 +55,14 @@ def _interpolated_housenumber(table: SaFromClause) -> SaLabel:
|
|||||||
sa.Integer).label('housenumber')
|
sa.Integer).label('housenumber')
|
||||||
|
|
||||||
|
|
||||||
|
def _interpolated_position(table: SaFromClause) -> SaLabel:
|
||||||
|
fac = sa.cast(table.c.step, sa.Float) / (table.c.endnumber - table.c.startnumber)
|
||||||
|
rounded_pos = sa.func.round(table.c.position / fac) * fac
|
||||||
|
return sa.case(
|
||||||
|
(table.c.endnumber == table.c.startnumber, table.c.linegeo.ST_Centroid()),
|
||||||
|
else_=table.c.linegeo.ST_LineInterpolatePoint(rounded_pos)).label('centroid')
|
||||||
|
|
||||||
|
|
||||||
def _is_address_point(table: SaFromClause) -> SaColumn:
|
def _is_address_point(table: SaFromClause) -> SaColumn:
|
||||||
return sa.and_(table.c.rank_address == 30,
|
return sa.and_(table.c.rank_address == 30,
|
||||||
sa.or_(table.c.housenumber != None,
|
sa.or_(table.c.housenumber != None,
|
||||||
@@ -203,8 +217,8 @@ class ReverseGeocoder:
|
|||||||
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,
|
||||||
_interpolated_housenumber(inner),
|
_interpolated_housenumber(inner),
|
||||||
|
_interpolated_position(inner),
|
||||||
inner.c.postcode, inner.c.country_code,
|
inner.c.postcode, inner.c.country_code,
|
||||||
inner.c.linegeo.ST_LineInterpolatePoint(inner.c.position).label('centroid'),
|
|
||||||
inner.c.distance)
|
inner.c.distance)
|
||||||
|
|
||||||
if self.details.geometry_output:
|
if self.details.geometry_output:
|
||||||
@@ -215,6 +229,7 @@ class ReverseGeocoder:
|
|||||||
|
|
||||||
|
|
||||||
async def _find_tiger_number_for_street(self, parent_place_id: int,
|
async def _find_tiger_number_for_street(self, parent_place_id: int,
|
||||||
|
parent_type: str, parent_id: int,
|
||||||
wkt: WKTElement) -> Optional[SaRow]:
|
wkt: WKTElement) -> Optional[SaRow]:
|
||||||
t = self.conn.t.tiger
|
t = self.conn.t.tiger
|
||||||
|
|
||||||
@@ -229,9 +244,11 @@ class ReverseGeocoder:
|
|||||||
|
|
||||||
sql = sa.select(inner.c.place_id,
|
sql = sa.select(inner.c.place_id,
|
||||||
inner.c.parent_place_id,
|
inner.c.parent_place_id,
|
||||||
|
sa.literal(parent_type).label('osm_type'),
|
||||||
|
sa.literal(parent_id).label('osm_id'),
|
||||||
_interpolated_housenumber(inner),
|
_interpolated_housenumber(inner),
|
||||||
|
_interpolated_position(inner),
|
||||||
inner.c.postcode,
|
inner.c.postcode,
|
||||||
inner.c.linegeo.ST_LineInterpolatePoint(inner.c.position).label('centroid'),
|
|
||||||
inner.c.distance)
|
inner.c.distance)
|
||||||
|
|
||||||
if self.details.geometry_output:
|
if self.details.geometry_output:
|
||||||
@@ -241,15 +258,16 @@ class ReverseGeocoder:
|
|||||||
return (await self.conn.execute(sql)).one_or_none()
|
return (await self.conn.execute(sql)).one_or_none()
|
||||||
|
|
||||||
|
|
||||||
async def lookup_street_poi(self, wkt: WKTElement) -> Optional[nres.ReverseResult]:
|
async def lookup_street_poi(self,
|
||||||
|
wkt: WKTElement) -> Tuple[Optional[SaRow], RowFunc]:
|
||||||
""" Find a street or POI/address for the given WKT point.
|
""" Find a street or POI/address for the given WKT point.
|
||||||
"""
|
"""
|
||||||
log().section('Reverse lookup on street/address level')
|
log().section('Reverse lookup on street/address level')
|
||||||
result = None
|
|
||||||
distance = 0.006
|
distance = 0.006
|
||||||
parent_place_id = None
|
parent_place_id = None
|
||||||
|
|
||||||
row = await self._find_closest_street_or_poi(wkt, distance)
|
row = await self._find_closest_street_or_poi(wkt, distance)
|
||||||
|
row_func: RowFunc = nres.create_from_placex_row
|
||||||
log().var_dump('Result (street/building)', row)
|
log().var_dump('Result (street/building)', row)
|
||||||
|
|
||||||
# If the closest result was a street, but an address was requested,
|
# If the closest result was a street, but an address was requested,
|
||||||
@@ -266,14 +284,19 @@ class ReverseGeocoder:
|
|||||||
|
|
||||||
if addr_row is not None:
|
if addr_row is not None:
|
||||||
row = addr_row
|
row = addr_row
|
||||||
|
row_func = nres.create_from_placex_row
|
||||||
distance = addr_row.distance
|
distance = addr_row.distance
|
||||||
elif row.country_code == 'us' and parent_place_id is not None:
|
elif row.country_code == 'us' and parent_place_id is not None:
|
||||||
log().comment('Find TIGER housenumber for street')
|
log().comment('Find TIGER housenumber for street')
|
||||||
addr_row = await self._find_tiger_number_for_street(parent_place_id, wkt)
|
addr_row = await self._find_tiger_number_for_street(parent_place_id,
|
||||||
|
row.osm_type,
|
||||||
|
row.osm_id,
|
||||||
|
wkt)
|
||||||
log().var_dump('Result (street Tiger housenumber)', addr_row)
|
log().var_dump('Result (street Tiger housenumber)', addr_row)
|
||||||
|
|
||||||
if addr_row is not None:
|
if addr_row is not None:
|
||||||
result = nres.create_from_tiger_row(addr_row, nres.ReverseResult)
|
row = addr_row
|
||||||
|
row_func = nres.create_from_tiger_row
|
||||||
else:
|
else:
|
||||||
distance = row.distance
|
distance = row.distance
|
||||||
|
|
||||||
@@ -285,9 +308,10 @@ class ReverseGeocoder:
|
|||||||
wkt, distance)
|
wkt, distance)
|
||||||
log().var_dump('Result (street interpolation)', addr_row)
|
log().var_dump('Result (street interpolation)', addr_row)
|
||||||
if addr_row is not None:
|
if addr_row is not None:
|
||||||
result = nres.create_from_osmline_row(addr_row, nres.ReverseResult)
|
row = addr_row
|
||||||
|
row_func = nres.create_from_osmline_row
|
||||||
|
|
||||||
return result or nres.create_from_placex_row(row, nres.ReverseResult)
|
return row, row_func
|
||||||
|
|
||||||
|
|
||||||
async def _lookup_area_address(self, wkt: WKTElement) -> Optional[SaRow]:
|
async def _lookup_area_address(self, wkt: WKTElement) -> Optional[SaRow]:
|
||||||
@@ -391,7 +415,7 @@ class ReverseGeocoder:
|
|||||||
return row
|
return row
|
||||||
|
|
||||||
|
|
||||||
async def lookup_area(self, wkt: WKTElement) -> Optional[nres.ReverseResult]:
|
async def lookup_area(self, wkt: WKTElement) -> Optional[SaRow]:
|
||||||
""" Lookup large areas for the given WKT point.
|
""" Lookup large areas for the given WKT point.
|
||||||
"""
|
"""
|
||||||
log().section('Reverse lookup by larger area features')
|
log().section('Reverse lookup by larger area features')
|
||||||
@@ -406,10 +430,10 @@ class ReverseGeocoder:
|
|||||||
else:
|
else:
|
||||||
other_row = None
|
other_row = None
|
||||||
|
|
||||||
return nres.create_from_placex_row(_get_closest(address_row, other_row), nres.ReverseResult)
|
return _get_closest(address_row, other_row)
|
||||||
|
|
||||||
|
|
||||||
async def lookup_country(self, wkt: WKTElement) -> Optional[nres.ReverseResult]:
|
async def lookup_country(self, wkt: WKTElement) -> Optional[SaRow]:
|
||||||
""" Lookup the country for the given WKT point.
|
""" Lookup the country for the given WKT point.
|
||||||
"""
|
"""
|
||||||
log().section('Reverse lookup by country code')
|
log().section('Reverse lookup by country code')
|
||||||
@@ -470,7 +494,7 @@ class ReverseGeocoder:
|
|||||||
|
|
||||||
address_row = (await self.conn.execute(sql)).one_or_none()
|
address_row = (await self.conn.execute(sql)).one_or_none()
|
||||||
|
|
||||||
return nres.create_from_placex_row(address_row, nres.ReverseResult)
|
return address_row
|
||||||
|
|
||||||
|
|
||||||
async def lookup(self, coord: AnyPoint) -> Optional[nres.ReverseResult]:
|
async def lookup(self, coord: AnyPoint) -> Optional[nres.ReverseResult]:
|
||||||
@@ -484,15 +508,24 @@ class ReverseGeocoder:
|
|||||||
|
|
||||||
wkt = WKTElement(f'POINT({coord[0]} {coord[1]})', srid=4326)
|
wkt = WKTElement(f'POINT({coord[0]} {coord[1]})', srid=4326)
|
||||||
|
|
||||||
result: Optional[nres.ReverseResult] = None
|
row: Optional[SaRow] = None
|
||||||
|
row_func: RowFunc = nres.create_from_placex_row
|
||||||
|
|
||||||
if self.max_rank >= 26:
|
if self.max_rank >= 26:
|
||||||
result = await self.lookup_street_poi(wkt)
|
row, tmp_row_func = await self.lookup_street_poi(wkt)
|
||||||
if result is None and self.max_rank > 4:
|
if row is not None:
|
||||||
result = await self.lookup_area(wkt)
|
row_func = tmp_row_func
|
||||||
if result is None and self.layer_enabled(DataLayer.ADDRESS):
|
if row is None and self.max_rank > 4:
|
||||||
result = await self.lookup_country(wkt)
|
row = await self.lookup_area(wkt)
|
||||||
|
if row is None and self.layer_enabled(DataLayer.ADDRESS):
|
||||||
|
row = await self.lookup_country(wkt)
|
||||||
|
|
||||||
|
result = row_func(row, nres.ReverseResult)
|
||||||
if result is not None:
|
if result is not None:
|
||||||
|
assert row is not None
|
||||||
|
result.distance = row.distance
|
||||||
|
if hasattr(row, 'bbox'):
|
||||||
|
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.details)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -12,12 +12,16 @@ version a more flexible formatting is required.
|
|||||||
"""
|
"""
|
||||||
from typing import Tuple, Optional, Mapping
|
from typing import Tuple, Optional, Mapping
|
||||||
|
|
||||||
|
import nominatim.api as napi
|
||||||
|
|
||||||
def get_label_tag(category: Tuple[str, str], extratags: Optional[Mapping[str, str]],
|
def get_label_tag(category: Tuple[str, str], extratags: Optional[Mapping[str, str]],
|
||||||
rank: int, country: Optional[str]) -> str:
|
rank: int, country: Optional[str]) -> str:
|
||||||
""" Create a label tag for the given place that can be used as an XML name.
|
""" Create a label tag for the given place that can be used as an XML name.
|
||||||
"""
|
"""
|
||||||
if rank < 26 and extratags and 'place'in extratags:
|
if rank < 26 and extratags and 'place' in extratags:
|
||||||
label = extratags['place']
|
label = extratags['place']
|
||||||
|
elif rank < 26 and extratags and 'linked_place' in extratags:
|
||||||
|
label = extratags['linked_place']
|
||||||
elif category == ('boundary', 'administrative'):
|
elif category == ('boundary', 'administrative'):
|
||||||
label = ADMIN_LABELS.get((country or '', int(rank/2)))\
|
label = ADMIN_LABELS.get((country or '', int(rank/2)))\
|
||||||
or ADMIN_LABELS.get(('', int(rank/2)))\
|
or ADMIN_LABELS.get(('', int(rank/2)))\
|
||||||
@@ -37,6 +41,30 @@ def get_label_tag(category: Tuple[str, str], extratags: Optional[Mapping[str, st
|
|||||||
return label.lower().replace(' ', '_')
|
return label.lower().replace(' ', '_')
|
||||||
|
|
||||||
|
|
||||||
|
def bbox_from_result(result: napi.ReverseResult) -> 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
|
||||||
|
search rank.
|
||||||
|
"""
|
||||||
|
if (result.osm_object and result.osm_object[0] == 'N') or result.bbox is None:
|
||||||
|
extent = NODE_EXTENT.get(result.category, 0.00005)
|
||||||
|
return napi.Bbox.from_point(result.centroid, extent)
|
||||||
|
|
||||||
|
return result.bbox
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
OSM_ATTRIBUTION = 'Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright'
|
||||||
|
|
||||||
|
|
||||||
|
OSM_TYPE_NAME = {
|
||||||
|
'N': 'node',
|
||||||
|
'W': 'way',
|
||||||
|
'R': 'relation'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
ADMIN_LABELS = {
|
ADMIN_LABELS = {
|
||||||
('', 1): 'Continent',
|
('', 1): 'Continent',
|
||||||
('', 2): 'Country',
|
('', 2): 'Country',
|
||||||
@@ -142,3 +170,31 @@ ICONS = {
|
|||||||
('amenity', 'prison'): 'amenity_prison',
|
('amenity', 'prison'): 'amenity_prison',
|
||||||
('highway', 'bus_stop'): 'transport_bus_stop2'
|
('highway', 'bus_stop'): 'transport_bus_stop2'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NODE_EXTENT = {
|
||||||
|
('place', 'continent'): 25,
|
||||||
|
('place', 'country'): 7,
|
||||||
|
('place', 'state'): 2.6,
|
||||||
|
('place', 'province'): 2.6,
|
||||||
|
('place', 'region'): 1.0,
|
||||||
|
('place', 'county'): 0.7,
|
||||||
|
('place', 'city'): 0.16,
|
||||||
|
('place', 'municipality'): 0.16,
|
||||||
|
('place', 'island'): 0.32,
|
||||||
|
('place', 'postcode'): 0.16,
|
||||||
|
('place', 'town'): 0.04,
|
||||||
|
('place', 'village'): 0.02,
|
||||||
|
('place', 'hamlet'): 0.02,
|
||||||
|
('place', 'district'): 0.02,
|
||||||
|
('place', 'borough'): 0.02,
|
||||||
|
('place', 'suburb'): 0.02,
|
||||||
|
('place', 'locality'): 0.01,
|
||||||
|
('place', 'neighbourhood'): 0.01,
|
||||||
|
('place', 'quarter'): 0.01,
|
||||||
|
('place', 'city_block'): 0.01,
|
||||||
|
('landuse', 'farm'): 0.01,
|
||||||
|
('place', 'farm'): 0.01,
|
||||||
|
('place', 'airport'): 0.015,
|
||||||
|
('aeroway', 'aerodrome'): 0.015,
|
||||||
|
('railway', 'station'): 0.005
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
# 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.
|
|
||||||
"""
|
|
||||||
Constants shared by all formats.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import nominatim.api as napi
|
|
||||||
|
|
||||||
# pylint: disable=line-too-long
|
|
||||||
OSM_ATTRIBUTION = 'Data © OpenStreetMap contributors, ODbL 1.0. http://www.openstreetmap.org/copyright'
|
|
||||||
|
|
||||||
OSM_TYPE_NAME = {
|
|
||||||
'N': 'node',
|
|
||||||
'W': 'way',
|
|
||||||
'R': 'relation'
|
|
||||||
}
|
|
||||||
|
|
||||||
NODE_EXTENT = [25, 25, 25, 25,
|
|
||||||
7,
|
|
||||||
2.6, 2.6, 2.0, 1.0, 1.0,
|
|
||||||
0.7, 0.7, 0.7,
|
|
||||||
0.16, 0.16, 0.16, 0.16,
|
|
||||||
0.04, 0.04,
|
|
||||||
0.02, 0.02,
|
|
||||||
0.01, 0.01, 0.01, 0.01, 0.01,
|
|
||||||
0.015, 0.015, 0.015, 0.015,
|
|
||||||
0.005]
|
|
||||||
|
|
||||||
|
|
||||||
def bbox_from_result(result: napi.ReverseResult) -> 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
|
|
||||||
search rank.
|
|
||||||
"""
|
|
||||||
if (result.osm_object and result.osm_object[0] == 'N') or result.bbox is None:
|
|
||||||
return napi.Bbox.from_point(result.centroid, NODE_EXTENT[result.rank_search])
|
|
||||||
|
|
||||||
return result.bbox
|
|
||||||
@@ -10,13 +10,12 @@ Helper functions for output of results in json formats.
|
|||||||
from typing import Mapping, Any, Optional, Tuple
|
from typing import Mapping, Any, Optional, Tuple
|
||||||
|
|
||||||
import nominatim.api as napi
|
import nominatim.api as napi
|
||||||
from nominatim.api.v1.constants import OSM_ATTRIBUTION, OSM_TYPE_NAME, bbox_from_result
|
import nominatim.api.v1.classtypes as cl
|
||||||
from nominatim.api.v1.classtypes import ICONS, get_label_tag
|
|
||||||
from nominatim.utils.json_writer import JsonWriter
|
from nominatim.utils.json_writer import JsonWriter
|
||||||
|
|
||||||
def _write_osm_id(out: JsonWriter, osm_object: Optional[Tuple[str, int]]) -> None:
|
def _write_osm_id(out: JsonWriter, osm_object: Optional[Tuple[str, int]]) -> None:
|
||||||
if osm_object is not None:
|
if osm_object is not None:
|
||||||
out.keyval_not_none('osm_type', OSM_TYPE_NAME.get(osm_object[0], None))\
|
out.keyval_not_none('osm_type', cl.OSM_TYPE_NAME.get(osm_object[0], None))\
|
||||||
.keyval('osm_id', osm_object[1])
|
.keyval('osm_id', osm_object[1])
|
||||||
|
|
||||||
|
|
||||||
@@ -24,11 +23,15 @@ def _write_typed_address(out: JsonWriter, address: Optional[napi.AddressLines],
|
|||||||
country_code: Optional[str]) -> None:
|
country_code: Optional[str]) -> None:
|
||||||
parts = {}
|
parts = {}
|
||||||
for line in (address or []):
|
for line in (address or []):
|
||||||
if line.isaddress and line.local_name:
|
if line.isaddress:
|
||||||
label = get_label_tag(line.category, line.extratags,
|
if line.local_name:
|
||||||
line.rank_address, country_code)
|
label = cl.get_label_tag(line.category, line.extratags,
|
||||||
if label not in parts:
|
line.rank_address, country_code)
|
||||||
parts[label] = line.local_name
|
if label not in parts:
|
||||||
|
print(label)
|
||||||
|
parts[label] = line.local_name
|
||||||
|
if line.names and 'ISO3166-2' in line.names and line.admin_level:
|
||||||
|
parts[f"ISO3166-2-lvl{line.admin_level}"] = line.names['ISO3166-2']
|
||||||
|
|
||||||
for k, v in parts.items():
|
for k, v in parts.items():
|
||||||
out.keyval(k, v)
|
out.keyval(k, v)
|
||||||
@@ -79,7 +82,7 @@ def format_base_json(results: napi.ReverseResults, #pylint: disable=too-many-bra
|
|||||||
|
|
||||||
out.start_object()\
|
out.start_object()\
|
||||||
.keyval_not_none('place_id', result.place_id)\
|
.keyval_not_none('place_id', result.place_id)\
|
||||||
.keyval('licence', OSM_ATTRIBUTION)\
|
.keyval('licence', cl.OSM_ATTRIBUTION)\
|
||||||
|
|
||||||
_write_osm_id(out, result.osm_object)
|
_write_osm_id(out, result.osm_object)
|
||||||
|
|
||||||
@@ -89,15 +92,15 @@ def format_base_json(results: napi.ReverseResults, #pylint: disable=too-many-bra
|
|||||||
.keyval('type', result.category[1])\
|
.keyval('type', result.category[1])\
|
||||||
.keyval('place_rank', result.rank_search)\
|
.keyval('place_rank', result.rank_search)\
|
||||||
.keyval('importance', result.calculated_importance())\
|
.keyval('importance', result.calculated_importance())\
|
||||||
.keyval('addresstype', get_label_tag(result.category, result.extratags,
|
.keyval('addresstype', cl.get_label_tag(result.category, result.extratags,
|
||||||
result.rank_address,
|
result.rank_address,
|
||||||
result.country_code))\
|
result.country_code))\
|
||||||
.keyval('name', locales.display_name(result.names))\
|
.keyval('name', locales.display_name(result.names))\
|
||||||
.keyval('display_name', ', '.join(label_parts))
|
.keyval('display_name', ', '.join(label_parts))
|
||||||
|
|
||||||
|
|
||||||
if options.get('icon_base_url', None):
|
if options.get('icon_base_url', None):
|
||||||
icon = ICONS.get(result.category)
|
icon = cl.ICONS.get(result.category)
|
||||||
if icon:
|
if icon:
|
||||||
out.keyval('icon', f"{options['icon_base_url']}/{icon}.p.20.png")
|
out.keyval('icon', f"{options['icon_base_url']}/{icon}.p.20.png")
|
||||||
|
|
||||||
@@ -112,12 +115,12 @@ def format_base_json(results: napi.ReverseResults, #pylint: disable=too-many-bra
|
|||||||
if options.get('namedetails', False):
|
if options.get('namedetails', False):
|
||||||
out.keyval('namedetails', result.names)
|
out.keyval('namedetails', result.names)
|
||||||
|
|
||||||
bbox = bbox_from_result(result)
|
bbox = cl.bbox_from_result(result)
|
||||||
out.key('boundingbox').start_array()\
|
out.key('boundingbox').start_array()\
|
||||||
.value(bbox.minlat).next()\
|
.value(f"{bbox.minlat:0.7f}").next()\
|
||||||
.value(bbox.maxlat).next()\
|
.value(f"{bbox.maxlat:0.7f}").next()\
|
||||||
.value(bbox.minlon).next()\
|
.value(f"{bbox.minlon:0.7f}").next()\
|
||||||
.value(bbox.maxlon).next()\
|
.value(f"{bbox.maxlon:0.7f}").next()\
|
||||||
.end_array().next()
|
.end_array().next()
|
||||||
|
|
||||||
if result.geometry:
|
if result.geometry:
|
||||||
@@ -153,7 +156,7 @@ def format_base_geojson(results: napi.ReverseResults,
|
|||||||
|
|
||||||
out.start_object()\
|
out.start_object()\
|
||||||
.keyval('type', 'FeatureCollection')\
|
.keyval('type', 'FeatureCollection')\
|
||||||
.keyval('licence', OSM_ATTRIBUTION)\
|
.keyval('licence', cl.OSM_ATTRIBUTION)\
|
||||||
.key('features').start_array()
|
.key('features').start_array()
|
||||||
|
|
||||||
for result in results:
|
for result in results:
|
||||||
@@ -174,9 +177,9 @@ def format_base_geojson(results: napi.ReverseResults,
|
|||||||
.keyval('category', result.category[0])\
|
.keyval('category', result.category[0])\
|
||||||
.keyval('type', result.category[1])\
|
.keyval('type', result.category[1])\
|
||||||
.keyval('importance', result.calculated_importance())\
|
.keyval('importance', result.calculated_importance())\
|
||||||
.keyval('addresstype', get_label_tag(result.category, result.extratags,
|
.keyval('addresstype', cl.get_label_tag(result.category, result.extratags,
|
||||||
result.rank_address,
|
result.rank_address,
|
||||||
result.country_code))\
|
result.country_code))\
|
||||||
.keyval('name', locales.display_name(result.names))\
|
.keyval('name', locales.display_name(result.names))\
|
||||||
.keyval('display_name', ', '.join(label_parts))
|
.keyval('display_name', ', '.join(label_parts))
|
||||||
|
|
||||||
@@ -193,8 +196,10 @@ def format_base_geojson(results: napi.ReverseResults,
|
|||||||
|
|
||||||
out.end_object().next() # properties
|
out.end_object().next() # properties
|
||||||
|
|
||||||
bbox = bbox_from_result(result)
|
out.key('bbox').start_array()
|
||||||
out.keyval('bbox', bbox.coords)
|
for coord in cl.bbox_from_result(result).coords:
|
||||||
|
out.float(coord, 7).next()
|
||||||
|
out.end_array().next()
|
||||||
|
|
||||||
out.key('geometry').raw(result.geometry.get('geojson')
|
out.key('geometry').raw(result.geometry.get('geojson')
|
||||||
or result.centroid.to_geojson()).next()
|
or result.centroid.to_geojson()).next()
|
||||||
@@ -221,7 +226,7 @@ def format_base_geocodejson(results: napi.ReverseResults,
|
|||||||
.keyval('type', 'FeatureCollection')\
|
.keyval('type', 'FeatureCollection')\
|
||||||
.key('geocoding').start_object()\
|
.key('geocoding').start_object()\
|
||||||
.keyval('version', '0.1.0')\
|
.keyval('version', '0.1.0')\
|
||||||
.keyval('attribution', OSM_ATTRIBUTION)\
|
.keyval('attribution', cl.OSM_ATTRIBUTION)\
|
||||||
.keyval('licence', 'ODbL')\
|
.keyval('licence', 'ODbL')\
|
||||||
.keyval_not_none('query', options.get('query'))\
|
.keyval_not_none('query', options.get('query'))\
|
||||||
.end_object().next()\
|
.end_object().next()\
|
||||||
@@ -245,9 +250,9 @@ def format_base_geocodejson(results: napi.ReverseResults,
|
|||||||
out.keyval('osm_key', result.category[0])\
|
out.keyval('osm_key', result.category[0])\
|
||||||
.keyval('osm_value', result.category[1])\
|
.keyval('osm_value', result.category[1])\
|
||||||
.keyval('type', GEOCODEJSON_RANKS[max(3, min(28, result.rank_address))])\
|
.keyval('type', GEOCODEJSON_RANKS[max(3, min(28, result.rank_address))])\
|
||||||
.keyval_not_none('accuracy', result.distance)\
|
.keyval_not_none('accuracy', result.distance, transform=int)\
|
||||||
.keyval('label', ', '.join(label_parts))\
|
.keyval('label', ', '.join(label_parts))\
|
||||||
.keyval_not_none('name', locales.display_name(result.names))\
|
.keyval_not_none('name', result.names, transform=locales.display_name)\
|
||||||
|
|
||||||
if options.get('addressdetails', False):
|
if options.get('addressdetails', False):
|
||||||
_write_geocodejson_address(out, result.address_rows, result.place_id,
|
_write_geocodejson_address(out, result.address_rows, result.place_id,
|
||||||
|
|||||||
@@ -12,18 +12,20 @@ import datetime as dt
|
|||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
import nominatim.api as napi
|
import nominatim.api as napi
|
||||||
from nominatim.api.v1.constants import OSM_ATTRIBUTION, OSM_TYPE_NAME, bbox_from_result
|
import nominatim.api.v1.classtypes as cl
|
||||||
from nominatim.api.v1.classtypes import ICONS, get_label_tag
|
|
||||||
|
|
||||||
def _write_xml_address(root: ET.Element, address: napi.AddressLines,
|
def _write_xml_address(root: ET.Element, address: napi.AddressLines,
|
||||||
country_code: Optional[str]) -> None:
|
country_code: Optional[str]) -> None:
|
||||||
parts = {}
|
parts = {}
|
||||||
for line in address:
|
for line in address:
|
||||||
if line.isaddress and line.local_name:
|
if line.isaddress:
|
||||||
label = get_label_tag(line.category, line.extratags,
|
if line.local_name:
|
||||||
line.rank_address, country_code)
|
label = cl.get_label_tag(line.category, line.extratags,
|
||||||
if label not in parts:
|
line.rank_address, country_code)
|
||||||
parts[label] = line.local_name
|
if label not in parts:
|
||||||
|
parts[label] = line.local_name
|
||||||
|
if line.names and 'ISO3166-2' in line.names and line.admin_level:
|
||||||
|
parts[f"ISO3166-2-lvl{line.admin_level}"] = line.names['ISO3166-2']
|
||||||
|
|
||||||
for k,v in parts.items():
|
for k,v in parts.items():
|
||||||
ET.SubElement(root, k).text = v
|
ET.SubElement(root, k).text = v
|
||||||
@@ -44,18 +46,21 @@ def _create_base_entry(result: napi.ReverseResult, #pylint: disable=too-many-bra
|
|||||||
if result.place_id is not None:
|
if result.place_id is not None:
|
||||||
place.set('place_id', str(result.place_id))
|
place.set('place_id', str(result.place_id))
|
||||||
if result.osm_object:
|
if result.osm_object:
|
||||||
osm_type = OSM_TYPE_NAME.get(result.osm_object[0], None)
|
osm_type = cl.OSM_TYPE_NAME.get(result.osm_object[0], None)
|
||||||
if osm_type is not None:
|
if osm_type is not None:
|
||||||
place.set('osm_type', osm_type)
|
place.set('osm_type', osm_type)
|
||||||
place.set('osm_id', str(result.osm_object[1]))
|
place.set('osm_id', str(result.osm_object[1]))
|
||||||
if result.names and 'ref' in result.names:
|
if result.names and 'ref' in result.names:
|
||||||
place.set('place_id', result.names['ref'])
|
place.set('ref', result.names['ref'])
|
||||||
place.set('lat', str(result.centroid.lat))
|
elif label_parts:
|
||||||
place.set('lon', str(result.centroid.lon))
|
# bug reproduced from PHP
|
||||||
|
place.set('ref', label_parts[0])
|
||||||
|
place.set('lat', f"{result.centroid.lat:.7f}")
|
||||||
|
place.set('lon', f"{result.centroid.lon:.7f}")
|
||||||
|
|
||||||
bbox = bbox_from_result(result)
|
bbox = cl.bbox_from_result(result)
|
||||||
place.set('boundingbox', ','.join(map(str, [bbox.minlat, bbox.maxlat,
|
place.set('boundingbox',
|
||||||
bbox.minlon, bbox.maxlon])))
|
f"{bbox.minlat:.7f},{bbox.maxlat:.7f},{bbox.minlon:.7f},{bbox.maxlon:.7f}")
|
||||||
|
|
||||||
place.set('place_rank', str(result.rank_search))
|
place.set('place_rank', str(result.rank_search))
|
||||||
place.set('address_rank', str(result.rank_address))
|
place.set('address_rank', str(result.rank_address))
|
||||||
@@ -92,7 +97,7 @@ def format_base_xml(results: napi.ReverseResults,
|
|||||||
|
|
||||||
root = ET.Element(xml_root_tag)
|
root = ET.Element(xml_root_tag)
|
||||||
root.set('timestamp', dt.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S +00:00'))
|
root.set('timestamp', dt.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S +00:00'))
|
||||||
root.set('attribution', OSM_ATTRIBUTION)
|
root.set('attribution', cl.OSM_ATTRIBUTION)
|
||||||
for k, v in xml_extra_info.items():
|
for k, v in xml_extra_info.items():
|
||||||
root.set(k, v)
|
root.set(k, v)
|
||||||
|
|
||||||
@@ -103,7 +108,7 @@ def format_base_xml(results: napi.ReverseResults,
|
|||||||
place = _create_base_entry(result, root, simple, locales)
|
place = _create_base_entry(result, root, simple, locales)
|
||||||
|
|
||||||
if not simple and options.get('icon_base_url', None):
|
if not simple and options.get('icon_base_url', None):
|
||||||
icon = ICONS.get(result.category)
|
icon = cl.ICONS.get(result.category)
|
||||||
if icon:
|
if icon:
|
||||||
place.set('icon', icon)
|
place.set('icon', icon)
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,10 @@
|
|||||||
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, TypeVar
|
from typing import Optional, Any, Type, Callable, NoReturn, cast
|
||||||
|
from functools import reduce
|
||||||
import abc
|
import abc
|
||||||
|
import math
|
||||||
|
|
||||||
from nominatim.config import Configuration
|
from nominatim.config import Configuration
|
||||||
import nominatim.api as napi
|
import nominatim.api as napi
|
||||||
@@ -22,8 +24,6 @@ CONTENT_TYPE = {
|
|||||||
'debug': 'text/html; charset=utf-8'
|
'debug': 'text/html; charset=utf-8'
|
||||||
}
|
}
|
||||||
|
|
||||||
ConvT = TypeVar('ConvT', int, float)
|
|
||||||
|
|
||||||
class ASGIAdaptor(abc.ABC):
|
class ASGIAdaptor(abc.ABC):
|
||||||
""" Adapter class for the different ASGI frameworks.
|
""" Adapter class for the different ASGI frameworks.
|
||||||
Wraps functionality over concrete requests and responses.
|
Wraps functionality over concrete requests and responses.
|
||||||
@@ -107,10 +107,9 @@ class ASGIAdaptor(abc.ABC):
|
|||||||
raise self.error(msg, status)
|
raise self.error(msg, status)
|
||||||
|
|
||||||
|
|
||||||
def _get_typed(self, name: str, dest_type: Type[ConvT], type_name: str,
|
def get_int(self, name: str, default: Optional[int] = None) -> int:
|
||||||
default: Optional[ConvT] = None) -> ConvT:
|
""" Return an input parameter as an int. Raises an exception if
|
||||||
""" Return an input parameter as the type 'dest_type'. Raises an
|
the parameter is given but not in an integer format.
|
||||||
exception if the parameter is given but not in the given format.
|
|
||||||
|
|
||||||
If 'default' is given, then it will be returned when the parameter
|
If 'default' is given, then it will be returned when the parameter
|
||||||
is missing completely. When 'default' is None, an error will be
|
is missing completely. When 'default' is None, an error will be
|
||||||
@@ -125,25 +124,14 @@ class ASGIAdaptor(abc.ABC):
|
|||||||
self.raise_error(f"Parameter '{name}' missing.")
|
self.raise_error(f"Parameter '{name}' missing.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
intval = dest_type(value)
|
intval = int(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.raise_error(f"Parameter '{name}' must be a {type_name}.")
|
self.raise_error(f"Parameter '{name}' must be a number.")
|
||||||
|
|
||||||
return intval
|
return intval
|
||||||
|
|
||||||
|
|
||||||
def get_int(self, name: str, default: Optional[int] = None) -> int:
|
def get_float(self, name: str, default: Optional[float] = None) -> float:
|
||||||
""" Return an input parameter as an int. Raises an exception if
|
|
||||||
the parameter is given but not in an integer format.
|
|
||||||
|
|
||||||
If 'default' is given, then it will be returned when the parameter
|
|
||||||
is missing completely. When 'default' is None, an error will be
|
|
||||||
raised on a missing parameter.
|
|
||||||
"""
|
|
||||||
return self._get_typed(name, int, 'number', default)
|
|
||||||
|
|
||||||
|
|
||||||
def get_float(self, name: str, default: Optional[float] = None) -> int:
|
|
||||||
""" Return an input parameter as a flaoting-point number. Raises an
|
""" Return an input parameter as a flaoting-point number. Raises an
|
||||||
exception if the parameter is given but not in an float format.
|
exception if the parameter is given but not in an float format.
|
||||||
|
|
||||||
@@ -151,7 +139,23 @@ class ASGIAdaptor(abc.ABC):
|
|||||||
is missing completely. When 'default' is None, an error will be
|
is missing completely. When 'default' is None, an error will be
|
||||||
raised on a missing parameter.
|
raised on a missing parameter.
|
||||||
"""
|
"""
|
||||||
return self._get_typed(name, float, 'number', default)
|
value = self.get(name)
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
if default is not None:
|
||||||
|
return default
|
||||||
|
|
||||||
|
self.raise_error(f"Parameter '{name}' missing.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
fval = float(value)
|
||||||
|
except ValueError:
|
||||||
|
self.raise_error(f"Parameter '{name}' must be a number.")
|
||||||
|
|
||||||
|
if math.isnan(fval) or math.isinf(fval):
|
||||||
|
self.raise_error(f"Parameter '{name}' must be a number.")
|
||||||
|
|
||||||
|
return fval
|
||||||
|
|
||||||
|
|
||||||
def get_bool(self, name: str, default: Optional[bool] = None) -> bool:
|
def get_bool(self, name: str, default: Optional[bool] = None) -> bool:
|
||||||
@@ -194,15 +198,16 @@ class ASGIAdaptor(abc.ABC):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_layers(self) -> napi.DataLayer:
|
def get_layers(self) -> Optional[napi.DataLayer]:
|
||||||
""" Return a parsed version of the layer parameter.
|
""" Return a parsed version of the layer parameter.
|
||||||
"""
|
"""
|
||||||
param = self.get('layer', None)
|
param = self.get('layer', None)
|
||||||
if param is None:
|
if param is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return reduce(napi.DataLayer.__or__,
|
return cast(napi.DataLayer,
|
||||||
(getattr(napi.DataLayer, s.upper()) for s in param.split(',')))
|
reduce(napi.DataLayer.__or__,
|
||||||
|
(getattr(napi.DataLayer, s.upper()) for s in param.split(','))))
|
||||||
|
|
||||||
|
|
||||||
def parse_format(self, result_type: Type[Any], default: str) -> str:
|
def parse_format(self, result_type: Type[Any], default: str) -> str:
|
||||||
@@ -289,10 +294,6 @@ async def reverse_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) ->
|
|||||||
|
|
||||||
zoom = max(0, min(18, params.get_int('zoom', 18)))
|
zoom = max(0, min(18, params.get_int('zoom', 18)))
|
||||||
|
|
||||||
# Negation makes sure that NaN is handled. Don't change.
|
|
||||||
if not abs(coord[0]) <= 180 or not abs(coord[1]) <= 90:
|
|
||||||
params.raise_error('Invalid coordinates.')
|
|
||||||
|
|
||||||
details = napi.LookupDetails(address_details=True,
|
details = napi.LookupDetails(address_details=True,
|
||||||
geometry_simplification=params.get_float('polygon_threshold', 0.0))
|
geometry_simplification=params.get_float('polygon_threshold', 0.0))
|
||||||
numgeoms = 0
|
numgeoms = 0
|
||||||
@@ -311,7 +312,7 @@ async def reverse_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) ->
|
|||||||
numgeoms += 1
|
numgeoms += 1
|
||||||
|
|
||||||
if numgeoms > params.config().get_int('POLYGON_OUTPUT_MAX_TYPES'):
|
if numgeoms > params.config().get_int('POLYGON_OUTPUT_MAX_TYPES'):
|
||||||
params.raise_error(f'Too many polgyon output options selected.')
|
params.raise_error('Too many polgyon output options selected.')
|
||||||
|
|
||||||
result = await api.reverse(coord, REVERSE_MAX_RANKS[zoom],
|
result = await api.reverse(coord, REVERSE_MAX_RANKS[zoom],
|
||||||
params.get_layers() or
|
params.get_layers() or
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ Server implementation using the falcon webserver framework.
|
|||||||
from typing import Optional, Mapping, cast, Any
|
from typing import Optional, Mapping, cast, Any
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import falcon
|
|
||||||
from falcon.asgi import App, Request, Response
|
from falcon.asgi import App, Request, Response
|
||||||
|
|
||||||
from nominatim.api import NominatimAPIAsync
|
from nominatim.api import NominatimAPIAsync
|
||||||
@@ -26,9 +25,12 @@ class HTTPNominatimError(Exception):
|
|||||||
self.content_type = content_type
|
self.content_type = content_type
|
||||||
|
|
||||||
|
|
||||||
async def nominatim_error_handler(req: Request, resp: Response,
|
async def nominatim_error_handler(req: Request, resp: Response, #pylint: disable=unused-argument
|
||||||
exception: HTTPNominatimError,
|
exception: HTTPNominatimError,
|
||||||
_: Any) -> None:
|
_: Any) -> None:
|
||||||
|
""" Special error handler that passes message and content type as
|
||||||
|
per exception info.
|
||||||
|
"""
|
||||||
resp.status = exception.status
|
resp.status = exception.status
|
||||||
resp.text = exception.msg
|
resp.text = exception.msg
|
||||||
resp.content_type = exception.content_type
|
resp.content_type = exception.content_type
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ class ParamWrapper(api_impl.ASGIAdaptor):
|
|||||||
|
|
||||||
def error(self, msg: str, status: int = 400) -> SanicException:
|
def error(self, msg: str, status: int = 400) -> SanicException:
|
||||||
exception = SanicException(msg, status_code=status)
|
exception = SanicException(msg, status_code=status)
|
||||||
exception.headers = {'content-type': self.content_type}
|
|
||||||
|
|
||||||
return exception
|
return exception
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,11 @@ class JsonWriter:
|
|||||||
return self.raw(json.dumps(value, ensure_ascii=False))
|
return self.raw(json.dumps(value, ensure_ascii=False))
|
||||||
|
|
||||||
|
|
||||||
|
def float(self, value: float, precision: int) -> 'JsonWriter':
|
||||||
|
""" Write out a float value with the given precision.
|
||||||
|
"""
|
||||||
|
return self.raw(f"{value:0.{precision}f}")
|
||||||
|
|
||||||
def next(self) -> 'JsonWriter':
|
def next(self) -> 'JsonWriter':
|
||||||
""" Write out a delimiter comma between JSON object or array elements.
|
""" Write out a delimiter comma between JSON object or array elements.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Feature: Geocodejson for Reverse API
|
|||||||
| Point | [9.5036065, 47.0660892] |
|
| Point | [9.5036065, 47.0660892] |
|
||||||
And results contain in field __geocoding
|
And results contain in field __geocoding
|
||||||
| version | licence | attribution |
|
| version | licence | attribution |
|
||||||
| 0.1.0 | ODbL | Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright |
|
| 0.1.0 | ODbL | ^Data © OpenStreetMap contributors, ODbL 1.0. https?://osm.org/copyright$ |
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
| has_address | attributes |
|
| has_address | attributes |
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ Feature: Geojson for Reverse API
|
|||||||
| way | 1 | 30 | place | house | place |
|
| way | 1 | 30 | place | house | place |
|
||||||
And results contain
|
And results contain
|
||||||
| boundingbox |
|
| boundingbox |
|
||||||
| [47.118495392, 47.118595392, 9.57049676, 9.57059676] |
|
| ^\[47.118495\d*, 47.118595\d*, 9.570496\d*, 9.570596\d*\] |
|
||||||
And results contain
|
And results contain
|
||||||
| display_name |
|
| display_name |
|
||||||
| 1019, Grosssteg, Sücka, Triesenberg, Oberland, 9497, Liechtenstein |
|
| 1019, Grosssteg, Sücka, Triesenberg, Oberland, 9497, Liechtenstein |
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ Feature: Json output for Reverse API
|
|||||||
| 1 | attributes |
|
| 1 | attributes |
|
||||||
| 0 | not attributes |
|
| 0 | not attributes |
|
||||||
|
|
||||||
Scenario Outline: Siple OSM result
|
Scenario Outline: Simple OSM result
|
||||||
When sending v1/reverse at 47.066,9.504 with format <format>
|
When sending v1/reverse at 47.066,9.504 with format <format>
|
||||||
Then result has attributes place_id
|
Then result has attributes place_id
|
||||||
And results contain
|
And results contain
|
||||||
| licence |
|
| licence |
|
||||||
| Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright |
|
| ^Data © OpenStreetMap contributors, ODbL 1.0. https?://osm.org/copyright$ |
|
||||||
And results contain
|
And results contain
|
||||||
| osm_type | osm_id |
|
| osm_type | osm_id |
|
||||||
| node | 6522627624 |
|
| node | 6522627624 |
|
||||||
@@ -62,7 +62,7 @@ Feature: Json output for Reverse API
|
|||||||
| way | 1 |
|
| way | 1 |
|
||||||
And results contain
|
And results contain
|
||||||
| centroid | boundingbox |
|
| centroid | boundingbox |
|
||||||
| 9.57054676 47.118545392 | ['47.118495392', '47.118595392', '9.57049676', '9.57059676'] |
|
| 9.57054676 47.118545392 | ^\['47.118495\d*', '47.118595\d*', '9.570496\d*', '9.570596\d*'\] |
|
||||||
And results contain
|
And results contain
|
||||||
| display_name |
|
| display_name |
|
||||||
| 1019, Grosssteg, Sücka, Triesenberg, Oberland, 9497, Liechtenstein |
|
| 1019, Grosssteg, Sücka, Triesenberg, Oberland, 9497, Liechtenstein |
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ Feature: XML output for Reverse API
|
|||||||
| way | 396009653 | 30 | 30 |
|
| way | 396009653 | 30 | 30 |
|
||||||
And results contain
|
And results contain
|
||||||
| centroid | boundingbox |
|
| centroid | boundingbox |
|
||||||
| -86.4808553258 32.4753580256 | ^32.475308025\d*,32.475408025\d*,-86.480905325\d*,-86.480805325\d* |
|
| -86.4808553 32.4753580 | ^32.4753080\d*,32.4754080\d*,-86.4809053\d*,-86.4808053\d* |
|
||||||
And results contain
|
And results contain
|
||||||
| display_name |
|
| display_name |
|
||||||
| 707, Upper Kingston Road, Upper Kingston, Prattville, Autauga County, 36067, United States |
|
| 707, Upper Kingston Road, Upper Kingston, Prattville, Autauga County, 36067, United States |
|
||||||
@@ -45,7 +45,7 @@ Feature: XML output for Reverse API
|
|||||||
| way | 1 | 30 | 30 |
|
| way | 1 | 30 | 30 |
|
||||||
And results contain
|
And results contain
|
||||||
| centroid | boundingbox |
|
| centroid | boundingbox |
|
||||||
| 9.57054676 47.118545392 | 47.118495392,47.118595392,9.57049676,9.57059676 |
|
| 9.57054676 47.118545392 | ^47.118495\d*,47.118595\d*,9.570496\d*,9.570596\d* |
|
||||||
And results contain
|
And results contain
|
||||||
| display_name |
|
| display_name |
|
||||||
| 1019, Grosssteg, Sücka, Triesenberg, Oberland, 9497, Liechtenstein |
|
| 1019, Grosssteg, Sücka, Triesenberg, Oberland, 9497, Liechtenstein |
|
||||||
|
|||||||
@@ -47,15 +47,16 @@ class Field:
|
|||||||
""" Generic comparator for fields, which looks at the type of the
|
""" Generic comparator for fields, which looks at the type of the
|
||||||
value compared.
|
value compared.
|
||||||
"""
|
"""
|
||||||
def __init__(self, value):
|
def __init__(self, value, **extra_args):
|
||||||
self.value = value
|
self.value = value
|
||||||
|
self.extra_args = extra_args
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if isinstance(self.value, float):
|
if isinstance(self.value, float):
|
||||||
return math.isclose(self.value, float(other))
|
return math.isclose(self.value, float(other), **self.extra_args)
|
||||||
|
|
||||||
if self.value.startswith('^'):
|
if self.value.startswith('^'):
|
||||||
return re.fullmatch(self.value, other)
|
return re.fullmatch(self.value, str(other))
|
||||||
|
|
||||||
if isinstance(other, dict):
|
if isinstance(other, dict):
|
||||||
return other == eval('{' + self.value + '}')
|
return other == eval('{' + self.value + '}')
|
||||||
|
|||||||
@@ -134,8 +134,8 @@ class GenericResponse:
|
|||||||
lon, lat = context.osm.grid_node(int(value))
|
lon, lat = context.osm.grid_node(int(value))
|
||||||
else:
|
else:
|
||||||
raise RuntimeError("Context needed when using grid coordinates")
|
raise RuntimeError("Context needed when using grid coordinates")
|
||||||
self.check_row_field(i, 'lat', Field(float(lat)), base=subdict)
|
self.check_row_field(i, 'lat', Field(float(lat), abs_tol=1e-07), base=subdict)
|
||||||
self.check_row_field(i, 'lon', Field(float(lon)), base=subdict)
|
self.check_row_field(i, 'lon', Field(float(lon), abs_tol=1e-07), base=subdict)
|
||||||
else:
|
else:
|
||||||
self.check_row_field(i, name, Field(value), base=subdict)
|
self.check_row_field(i, name, Field(value), base=subdict)
|
||||||
|
|
||||||
|
|||||||
@@ -229,7 +229,8 @@ def validate_result_number(context, operator, number):
|
|||||||
@then(u'a HTTP (?P<status>\d+) is returned')
|
@then(u'a HTTP (?P<status>\d+) is returned')
|
||||||
def check_http_return_status(context, status):
|
def check_http_return_status(context, status):
|
||||||
assert context.response.errorcode == int(status), \
|
assert context.response.errorcode == int(status), \
|
||||||
f"Return HTTP status is {context.response.errorcode}."
|
f"Return HTTP status is {context.response.errorcode}."\
|
||||||
|
f" Full response:\n{context.response.page}"
|
||||||
|
|
||||||
@then(u'the page contents equals "(?P<text>.+)"')
|
@then(u'the page contents equals "(?P<text>.+)"')
|
||||||
def check_page_content_equals(context, text):
|
def check_page_content_equals(context, text):
|
||||||
|
|||||||
@@ -378,6 +378,10 @@ def test_lookup_in_tiger(apiobj):
|
|||||||
startnumber=1, endnumber=4, step=1,
|
startnumber=1, endnumber=4, step=1,
|
||||||
postcode='34425',
|
postcode='34425',
|
||||||
geometry='LINESTRING(23 34, 23 35)')
|
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())
|
result = apiobj.api.lookup(napi.PlaceID(4924), napi.LookupDetails())
|
||||||
|
|
||||||
@@ -390,7 +394,7 @@ def test_lookup_in_tiger(apiobj):
|
|||||||
assert result.place_id == 4924
|
assert result.place_id == 4924
|
||||||
assert result.parent_place_id == 12
|
assert result.parent_place_id == 12
|
||||||
assert result.linked_place_id is None
|
assert result.linked_place_id is None
|
||||||
assert result.osm_object is None
|
assert result.osm_object == ('W', 6601223)
|
||||||
assert result.admin_level == 15
|
assert result.admin_level == 15
|
||||||
|
|
||||||
assert result.names is None
|
assert result.names is None
|
||||||
|
|||||||
@@ -58,9 +58,9 @@ def test_create_row_none(func):
|
|||||||
@pytest.mark.parametrize('func', (nresults.create_from_osmline_row,
|
@pytest.mark.parametrize('func', (nresults.create_from_osmline_row,
|
||||||
nresults.create_from_tiger_row))
|
nresults.create_from_tiger_row))
|
||||||
def test_create_row_with_housenumber(func):
|
def test_create_row_with_housenumber(func):
|
||||||
row = FakeRow(place_id = 2345, osm_id = 111, housenumber = 4,
|
row = FakeRow(place_id=2345, osm_type='W', osm_id=111, housenumber=4,
|
||||||
address = None, postcode = '99900', country_code = 'xd',
|
address=None, postcode='99900', country_code='xd',
|
||||||
centroid = FakeCentroid(0, 0))
|
centroid=FakeCentroid(0, 0))
|
||||||
|
|
||||||
res = func(row, DetailedResult)
|
res = func(row, DetailedResult)
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ def test_create_row_with_housenumber(func):
|
|||||||
@pytest.mark.parametrize('func', (nresults.create_from_osmline_row,
|
@pytest.mark.parametrize('func', (nresults.create_from_osmline_row,
|
||||||
nresults.create_from_tiger_row))
|
nresults.create_from_tiger_row))
|
||||||
def test_create_row_without_housenumber(func):
|
def test_create_row_without_housenumber(func):
|
||||||
row = FakeRow(place_id=2345, osm_id=111,
|
row = FakeRow(place_id=2345, osm_type='W', osm_id=111,
|
||||||
startnumber=1, endnumber=11, step=2,
|
startnumber=1, endnumber=11, step=2,
|
||||||
address=None, postcode='99900', country_code='xd',
|
address=None, postcode='99900', country_code='xd',
|
||||||
centroid=FakeCentroid(0, 0))
|
centroid=FakeCentroid(0, 0))
|
||||||
|
|||||||
Reference in New Issue
Block a user