switch reverse() to new Geometry datatype

Also switches to using bind parameters for recurring parameters.
This commit is contained in:
Sarah Hoffmann
2023-06-25 14:02:00 +02:00
parent 4bb4db0668
commit 6c4c9ec1f2
8 changed files with 127 additions and 100 deletions

View File

@@ -38,7 +38,7 @@ class SearchConnection:
) -> Any: ) -> Any:
""" Execute a 'scalar()' query on the connection. """ Execute a 'scalar()' query on the connection.
""" """
log().sql(self.connection, sql) log().sql(self.connection, sql, params)
return await self.connection.scalar(sql, params) return await self.connection.scalar(sql, params)
@@ -47,7 +47,7 @@ class SearchConnection:
) -> 'sa.Result[Any]': ) -> 'sa.Result[Any]':
""" Execute a 'execute()' query on the connection. """ Execute a 'execute()' query on the connection.
""" """
log().sql(self.connection, sql) log().sql(self.connection, sql, params)
return await self.connection.execute(sql, params) return await self.connection.execute(sql, params)

View File

@@ -66,7 +66,8 @@ class NominatimAPIAsync:
username=dsn.get('user'), password=dsn.get('password'), username=dsn.get('user'), password=dsn.get('password'),
host=dsn.get('host'), port=int(dsn['port']) if 'port' in dsn else None, host=dsn.get('host'), port=int(dsn['port']) if 'port' in dsn else None,
query=query) query=query)
engine = sa_asyncio.create_async_engine(dburl, future=True) engine = sa_asyncio.create_async_engine(dburl, future=True,
echo=self.config.get_bool('DEBUG_SQL'))
try: try:
async with engine.begin() as conn: async with engine.begin() as conn:

View File

@@ -7,7 +7,7 @@
""" """
Functions for specialised logging with HTML output. Functions for specialised logging with HTML output.
""" """
from typing import Any, Iterator, Optional, List, Tuple, cast from typing import Any, Iterator, Optional, List, Tuple, cast, Union, Mapping, Sequence
from contextvars import ContextVar from contextvars import ContextVar
import datetime as dt import datetime as dt
import textwrap import textwrap
@@ -74,22 +74,26 @@ class BaseLogger:
""" """
def sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> None: def sql(self, conn: AsyncConnection, statement: 'sa.Executable',
params: Union[Mapping[str, Any], Sequence[Mapping[str, Any]], None]) -> None:
""" Print the SQL for the given statement. """ Print the SQL for the given statement.
""" """
def format_sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> str: def format_sql(self, conn: AsyncConnection, statement: 'sa.Executable',
extra_params: Union[Mapping[str, Any], Sequence[Mapping[str, Any]], None]) -> str:
""" Return the comiled version of the statement. """ Return the comiled version of the statement.
""" """
try: compiled = cast('sa.ClauseElement', statement).compile(conn.sync_engine)
return str(cast('sa.ClauseElement', statement)
.compile(conn.sync_engine, compile_kwargs={"literal_binds": True}))
except sa.exc.CompileError:
pass
except NotImplementedError:
pass
return str(cast('sa.ClauseElement', statement).compile(conn.sync_engine)) params = dict(compiled.params)
if isinstance(extra_params, Mapping):
for k, v in extra_params.items():
params[k] = str(v)
elif isinstance(extra_params, Sequence) and extra_params:
for k in extra_params[0]:
params[k] = f':{k}'
return str(compiled) % params
class HTMLLogger(BaseLogger): class HTMLLogger(BaseLogger):
@@ -183,9 +187,10 @@ class HTMLLogger(BaseLogger):
self._write(f'</dl><b>TOTAL:</b> {total}</p>') self._write(f'</dl><b>TOTAL:</b> {total}</p>')
def sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> None: def sql(self, conn: AsyncConnection, statement: 'sa.Executable',
params: Union[Mapping[str, Any], Sequence[Mapping[str, Any]], None]) -> None:
self._timestamp() self._timestamp()
sqlstr = self.format_sql(conn, statement) sqlstr = self.format_sql(conn, statement, params)
if CODE_HIGHLIGHT: if CODE_HIGHLIGHT:
sqlstr = highlight(sqlstr, PostgresLexer(), sqlstr = highlight(sqlstr, PostgresLexer(),
HtmlFormatter(nowrap=True, lineseparator='<br />')) HtmlFormatter(nowrap=True, lineseparator='<br />'))
@@ -276,8 +281,9 @@ class TextLogger(BaseLogger):
self._write(f'TOTAL: {total}\n\n') self._write(f'TOTAL: {total}\n\n')
def sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> None: def sql(self, conn: AsyncConnection, statement: 'sa.Executable',
sqlstr = '\n| '.join(textwrap.wrap(self.format_sql(conn, statement), width=78)) params: Union[Mapping[str, Any], Sequence[Mapping[str, Any]], None]) -> None:
sqlstr = '\n| '.join(textwrap.wrap(self.format_sql(conn, statement, params), width=78))
self._write(f"| {sqlstr}\n\n") self._write(f"| {sqlstr}\n\n")

View File

@@ -263,7 +263,7 @@ def create_from_placex_row(row: Optional[SaRow],
rank_search=row.rank_search, rank_search=row.rank_search,
importance=row.importance, importance=row.importance,
country_code=row.country_code, country_code=row.country_code,
centroid=Point.from_wkb(row.centroid.data), centroid=Point.from_wkb(row.centroid),
geometry=_filter_geometries(row)) geometry=_filter_geometries(row))
@@ -288,7 +288,7 @@ def create_from_osmline_row(row: Optional[SaRow],
address=row.address, address=row.address,
postcode=row.postcode, postcode=row.postcode,
country_code=row.country_code, country_code=row.country_code,
centroid=Point.from_wkb(row.centroid.data), centroid=Point.from_wkb(row.centroid),
geometry=_filter_geometries(row)) geometry=_filter_geometries(row))
if hnr is None: if hnr is None:
@@ -321,7 +321,7 @@ def create_from_tiger_row(row: Optional[SaRow],
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',
centroid=Point.from_wkb(row.centroid.data), centroid=Point.from_wkb(row.centroid),
geometry=_filter_geometries(row)) geometry=_filter_geometries(row))
if hnr is None: if hnr is None:
@@ -350,7 +350,7 @@ def create_from_postcode_row(row: Optional[SaRow],
rank_search=row.rank_search, rank_search=row.rank_search,
rank_address=row.rank_address, rank_address=row.rank_address,
country_code=row.country_code, country_code=row.country_code,
centroid=Point.from_wkb(row.centroid.data), centroid=Point.from_wkb(row.centroid),
geometry=_filter_geometries(row)) geometry=_filter_geometries(row))
@@ -365,7 +365,7 @@ def create_from_country_row(row: Optional[SaRow],
return class_type(source_table=SourceTable.COUNTRY, return class_type(source_table=SourceTable.COUNTRY,
category=('place', 'country'), category=('place', 'country'),
centroid=Point.from_wkb(row.centroid.data), centroid=Point.from_wkb(row.centroid),
names=row.name, names=row.name,
rank_address=4, rank_search=4, rank_address=4, rank_search=4,
country_code=row.country_code) country_code=row.country_code)

View File

@@ -16,6 +16,7 @@ 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, ReverseDetails, GeometryFormat, Bbox from nominatim.api.types import AnyPoint, DataLayer, ReverseDetails, GeometryFormat, Bbox
from nominatim.db.sqlalchemy_types import Geometry
# 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.
@@ -23,16 +24,19 @@ from nominatim.api.types import AnyPoint, DataLayer, ReverseDetails, GeometryFor
RowFunc = Callable[[Optional[SaRow], Type[nres.ReverseResult]], Optional[nres.ReverseResult]] RowFunc = Callable[[Optional[SaRow], Type[nres.ReverseResult]], Optional[nres.ReverseResult]]
def _select_from_placex(t: SaFromClause, wkt: Optional[str] = None) -> SaSelect: WKT_PARAM = sa.bindparam('wkt', type_=Geometry)
MAX_RANK_PARAM = sa.bindparam('max_rank')
def _select_from_placex(t: SaFromClause, use_wkt: bool = True) -> SaSelect:
""" Create a select statement with the columns relevant for reverse """ Create a select statement with the columns relevant for reverse
results. results.
""" """
if wkt is None: if not use_wkt:
distance = t.c.distance distance = t.c.distance
centroid = t.c.centroid centroid = t.c.centroid
else: else:
distance = t.c.geometry.ST_Distance(wkt) distance = t.c.geometry.ST_Distance(WKT_PARAM)
centroid = sa.case((t.c.geometry.is_line_like(), t.c.geometry.ST_ClosestPoint(wkt)), centroid = sa.case((t.c.geometry.is_line_like(), t.c.geometry.ST_ClosestPoint(WKT_PARAM)),
else_=t.c.centroid).label('centroid') else_=t.c.centroid).label('centroid')
@@ -62,10 +66,11 @@ def _interpolated_position(table: SaFromClause) -> SaLabel:
else_=table.c.linegeo.ST_LineInterpolatePoint(rounded_pos)).label('centroid') else_=table.c.linegeo.ST_LineInterpolatePoint(rounded_pos)).label('centroid')
def _locate_interpolation(table: SaFromClause, wkt: str) -> SaLabel: def _locate_interpolation(table: SaFromClause) -> SaLabel:
""" Given a position, locate the closest point on the line. """ Given a position, locate the closest point on the line.
""" """
return sa.case((table.c.linegeo.is_line_like(), table.c.linegeo.ST_LineLocatePoint(wkt)), return sa.case((table.c.linegeo.is_line_like(),
table.c.linegeo.ST_LineLocatePoint(WKT_PARAM)),
else_=0).label('position') else_=0).label('position')
@@ -74,9 +79,11 @@ def _is_address_point(table: SaFromClause) -> SaColumn:
sa.or_(table.c.housenumber != None, sa.or_(table.c.housenumber != None,
table.c.name.has_key('housename'))) table.c.name.has_key('housename')))
def _get_closest(*rows: Optional[SaRow]) -> Optional[SaRow]: def _get_closest(*rows: Optional[SaRow]) -> Optional[SaRow]:
return min(rows, key=lambda row: 1000 if row is None else row.distance) return min(rows, key=lambda row: 1000 if row is None else row.distance)
class ReverseGeocoder: class ReverseGeocoder:
""" Class implementing the logic for looking up a place from a """ Class implementing the logic for looking up a place from a
coordinate. coordinate.
@@ -86,6 +93,8 @@ class ReverseGeocoder:
self.conn = conn self.conn = conn
self.params = params self.params = params
self.bind_params = {'max_rank': params.max_rank}
@property @property
def max_rank(self) -> int: def max_rank(self) -> int:
@@ -117,6 +126,7 @@ 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.has_geometries(): if not self.has_geometries():
return sql return sql
@@ -155,19 +165,18 @@ class ReverseGeocoder:
return table.c.class_.in_(tuple(include)) return table.c.class_.in_(tuple(include))
async def _find_closest_street_or_poi(self, wkt: str, async def _find_closest_street_or_poi(self, distance: float) -> Optional[SaRow]:
distance: float) -> Optional[SaRow]:
""" Look up the closest rank 26+ place in the database, which """ Look up the closest rank 26+ place in the database, which
is closer than the given distance. is closer than the given distance.
""" """
t = self.conn.t.placex t = self.conn.t.placex
sql = _select_from_placex(t, wkt)\ sql = _select_from_placex(t)\
.where(t.c.geometry.ST_DWithin(wkt, distance))\ .where(t.c.geometry.ST_DWithin(WKT_PARAM, distance))\
.where(t.c.indexed_status == 0)\ .where(t.c.indexed_status == 0)\
.where(t.c.linked_place_id == None)\ .where(t.c.linked_place_id == None)\
.where(sa.or_(sa.not_(t.c.geometry.is_area()), .where(sa.or_(sa.not_(t.c.geometry.is_area()),
t.c.centroid.ST_Distance(wkt) < distance))\ t.c.centroid.ST_Distance(WKT_PARAM) < distance))\
.order_by('distance')\ .order_by('distance')\
.limit(1) .limit(1)
@@ -185,22 +194,23 @@ class ReverseGeocoder:
t.c.class_.not_in(('place', 'building')), t.c.class_.not_in(('place', 'building')),
sa.not_(t.c.geometry.is_line_like()))) sa.not_(t.c.geometry.is_line_like())))
if self.has_feature_layers(): if self.has_feature_layers():
restrict.append(sa.and_(t.c.rank_search.between(26, self.max_rank), restrict.append(sa.and_(t.c.rank_search.between(26, MAX_RANK_PARAM),
t.c.rank_address == 0, t.c.rank_address == 0,
self._filter_by_layer(t))) self._filter_by_layer(t)))
if not restrict: if not restrict:
return None return None
return (await self.conn.execute(sql.where(sa.or_(*restrict)))).one_or_none() sql = sql.where(sa.or_(*restrict))
return (await self.conn.execute(sql, self.bind_params)).one_or_none()
async def _find_housenumber_for_street(self, parent_place_id: int, async def _find_housenumber_for_street(self, parent_place_id: int) -> Optional[SaRow]:
wkt: str) -> Optional[SaRow]:
t = self.conn.t.placex t = self.conn.t.placex
sql = _select_from_placex(t, wkt)\ sql = _select_from_placex(t)\
.where(t.c.geometry.ST_DWithin(wkt, 0.001))\ .where(t.c.geometry.ST_DWithin(WKT_PARAM, 0.001))\
.where(t.c.parent_place_id == parent_place_id)\ .where(t.c.parent_place_id == parent_place_id)\
.where(_is_address_point(t))\ .where(_is_address_point(t))\
.where(t.c.indexed_status == 0)\ .where(t.c.indexed_status == 0)\
@@ -210,18 +220,17 @@ class ReverseGeocoder:
sql = self._add_geometry_columns(sql, t.c.geometry) sql = self._add_geometry_columns(sql, t.c.geometry)
return (await self.conn.execute(sql)).one_or_none() return (await self.conn.execute(sql, self.bind_params)).one_or_none()
async def _find_interpolation_for_street(self, parent_place_id: Optional[int], async def _find_interpolation_for_street(self, parent_place_id: Optional[int],
wkt: str,
distance: float) -> Optional[SaRow]: distance: float) -> Optional[SaRow]:
t = self.conn.t.osmline t = self.conn.t.osmline
sql = sa.select(t, sql = sa.select(t,
t.c.linegeo.ST_Distance(wkt).label('distance'), t.c.linegeo.ST_Distance(WKT_PARAM).label('distance'),
_locate_interpolation(t, wkt))\ _locate_interpolation(t))\
.where(t.c.linegeo.ST_DWithin(wkt, distance))\ .where(t.c.linegeo.ST_DWithin(WKT_PARAM, distance))\
.where(t.c.startnumber != None)\ .where(t.c.startnumber != None)\
.order_by('distance')\ .order_by('distance')\
.limit(1) .limit(1)
@@ -242,18 +251,18 @@ class ReverseGeocoder:
sub = sql.subquery('geom') sub = sql.subquery('geom')
sql = self._add_geometry_columns(sa.select(sub), 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, self.bind_params)).one_or_none()
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, parent_type: str,
wkt: str) -> Optional[SaRow]: parent_id: int) -> Optional[SaRow]:
t = self.conn.t.tiger t = self.conn.t.tiger
inner = sa.select(t, inner = sa.select(t,
t.c.linegeo.ST_Distance(wkt).label('distance'), t.c.linegeo.ST_Distance(WKT_PARAM).label('distance'),
_locate_interpolation(t, wkt))\ _locate_interpolation(t))\
.where(t.c.linegeo.ST_DWithin(wkt, 0.001))\ .where(t.c.linegeo.ST_DWithin(WKT_PARAM, 0.001))\
.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)\
@@ -272,18 +281,17 @@ class ReverseGeocoder:
sub = sql.subquery('geom') sub = sql.subquery('geom')
sql = self._add_geometry_columns(sa.select(sub), 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, self.bind_params)).one_or_none()
async def lookup_street_poi(self, async def lookup_street_poi(self) -> Tuple[Optional[SaRow], RowFunc]:
wkt: str) -> 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')
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(distance)
row_func: RowFunc = nres.create_from_placex_row row_func: RowFunc = nres.create_from_placex_row
log().var_dump('Result (street/building)', row) log().var_dump('Result (street/building)', row)
@@ -296,7 +304,7 @@ class ReverseGeocoder:
distance = 0.001 distance = 0.001
parent_place_id = row.place_id parent_place_id = row.place_id
log().comment('Find housenumber for street') log().comment('Find housenumber for street')
addr_row = await self._find_housenumber_for_street(parent_place_id, wkt) addr_row = await self._find_housenumber_for_street(parent_place_id)
log().var_dump('Result (street housenumber)', addr_row) log().var_dump('Result (street housenumber)', addr_row)
if addr_row is not None: if addr_row is not None:
@@ -307,8 +315,7 @@ class ReverseGeocoder:
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, addr_row = await self._find_tiger_number_for_street(parent_place_id,
row.osm_type, row.osm_type,
row.osm_id, 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:
@@ -322,7 +329,7 @@ class ReverseGeocoder:
if self.max_rank > 27 and self.layer_enabled(DataLayer.ADDRESS): if self.max_rank > 27 and self.layer_enabled(DataLayer.ADDRESS):
log().comment('Find interpolation for street') log().comment('Find interpolation for street')
addr_row = await self._find_interpolation_for_street(parent_place_id, addr_row = await self._find_interpolation_for_street(parent_place_id,
wkt, distance) 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:
row = addr_row row = addr_row
@@ -331,7 +338,7 @@ class ReverseGeocoder:
return row, row_func return row, row_func
async def _lookup_area_address(self, wkt: str) -> Optional[SaRow]: async def _lookup_area_address(self) -> Optional[SaRow]:
""" Lookup large addressable areas for the given WKT point. """ Lookup large addressable areas for the given WKT point.
""" """
log().comment('Reverse lookup by larger address area features') log().comment('Reverse lookup by larger address area features')
@@ -340,10 +347,10 @@ class ReverseGeocoder:
# The inner SQL brings results in the right order, so that # The inner SQL brings results in the right order, so that
# later only a minimum of results needs to be checked with ST_Contains. # later only a minimum of results needs to be checked with ST_Contains.
inner = sa.select(t, sa.literal(0.0).label('distance'))\ inner = sa.select(t, sa.literal(0.0).label('distance'))\
.where(t.c.rank_search.between(5, self.max_rank))\ .where(t.c.rank_search.between(5, MAX_RANK_PARAM))\
.where(t.c.rank_address.between(5, 25))\ .where(t.c.rank_address.between(5, 25))\
.where(t.c.geometry.is_area())\ .where(t.c.geometry.is_area())\
.where(t.c.geometry.intersects(wkt))\ .where(t.c.geometry.intersects(WKT_PARAM))\
.where(t.c.name != None)\ .where(t.c.name != None)\
.where(t.c.indexed_status == 0)\ .where(t.c.indexed_status == 0)\
.where(t.c.linked_place_id == None)\ .where(t.c.linked_place_id == None)\
@@ -352,23 +359,23 @@ class ReverseGeocoder:
.limit(50)\ .limit(50)\
.subquery('area') .subquery('area')
sql = _select_from_placex(inner)\ sql = _select_from_placex(inner, False)\
.where(inner.c.geometry.ST_Contains(wkt))\ .where(inner.c.geometry.ST_Contains(WKT_PARAM))\
.order_by(sa.desc(inner.c.rank_search))\ .order_by(sa.desc(inner.c.rank_search))\
.limit(1) .limit(1)
sql = self._add_geometry_columns(sql, inner.c.geometry) sql = self._add_geometry_columns(sql, inner.c.geometry)
address_row = (await self.conn.execute(sql)).one_or_none() address_row = (await self.conn.execute(sql, self.bind_params)).one_or_none()
log().var_dump('Result (area)', address_row) log().var_dump('Result (area)', address_row)
if address_row is not None and address_row.rank_search < self.max_rank: if address_row is not None and address_row.rank_search < self.max_rank:
log().comment('Search for better matching place nodes inside the area') log().comment('Search for better matching place nodes inside the area')
inner = sa.select(t, inner = sa.select(t,
t.c.geometry.ST_Distance(wkt).label('distance'))\ t.c.geometry.ST_Distance(WKT_PARAM).label('distance'))\
.where(t.c.osm_type == 'N')\ .where(t.c.osm_type == 'N')\
.where(t.c.rank_search > address_row.rank_search)\ .where(t.c.rank_search > address_row.rank_search)\
.where(t.c.rank_search <= self.max_rank)\ .where(t.c.rank_search <= MAX_RANK_PARAM)\
.where(t.c.rank_address.between(5, 25))\ .where(t.c.rank_address.between(5, 25))\
.where(t.c.name != None)\ .where(t.c.name != None)\
.where(t.c.indexed_status == 0)\ .where(t.c.indexed_status == 0)\
@@ -376,13 +383,13 @@ class ReverseGeocoder:
.where(t.c.type != 'postcode')\ .where(t.c.type != 'postcode')\
.where(t.c.geometry .where(t.c.geometry
.ST_Buffer(sa.func.reverse_place_diameter(t.c.rank_search)) .ST_Buffer(sa.func.reverse_place_diameter(t.c.rank_search))
.intersects(wkt))\ .intersects(WKT_PARAM))\
.order_by(sa.desc(t.c.rank_search))\ .order_by(sa.desc(t.c.rank_search))\
.limit(50)\ .limit(50)\
.subquery('places') .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, False)\
.join(touter, touter.c.geometry.ST_Contains(inner.c.geometry))\ .join(touter, touter.c.geometry.ST_Contains(inner.c.geometry))\
.where(touter.c.place_id == address_row.place_id)\ .where(touter.c.place_id == address_row.place_id)\
.where(inner.c.distance < sa.func.reverse_place_diameter(inner.c.rank_search))\ .where(inner.c.distance < sa.func.reverse_place_diameter(inner.c.rank_search))\
@@ -391,7 +398,7 @@ class ReverseGeocoder:
sql = self._add_geometry_columns(sql, inner.c.geometry) sql = self._add_geometry_columns(sql, inner.c.geometry)
place_address_row = (await self.conn.execute(sql)).one_or_none() place_address_row = (await self.conn.execute(sql, self.bind_params)).one_or_none()
log().var_dump('Result (place node)', place_address_row) log().var_dump('Result (place node)', place_address_row)
if place_address_row is not None: if place_address_row is not None:
@@ -400,64 +407,64 @@ class ReverseGeocoder:
return address_row return address_row
async def _lookup_area_others(self, wkt: str) -> Optional[SaRow]: async def _lookup_area_others(self) -> Optional[SaRow]:
t = self.conn.t.placex t = self.conn.t.placex
inner = sa.select(t, t.c.geometry.ST_Distance(wkt).label('distance'))\ inner = sa.select(t, t.c.geometry.ST_Distance(WKT_PARAM).label('distance'))\
.where(t.c.rank_address == 0)\ .where(t.c.rank_address == 0)\
.where(t.c.rank_search.between(5, self.max_rank))\ .where(t.c.rank_search.between(5, MAX_RANK_PARAM))\
.where(t.c.name != None)\ .where(t.c.name != None)\
.where(t.c.indexed_status == 0)\ .where(t.c.indexed_status == 0)\
.where(t.c.linked_place_id == None)\ .where(t.c.linked_place_id == None)\
.where(self._filter_by_layer(t))\ .where(self._filter_by_layer(t))\
.where(t.c.geometry .where(t.c.geometry
.ST_Buffer(sa.func.reverse_place_diameter(t.c.rank_search)) .ST_Buffer(sa.func.reverse_place_diameter(t.c.rank_search))
.intersects(wkt))\ .intersects(WKT_PARAM))\
.order_by(sa.desc(t.c.rank_search))\ .order_by(sa.desc(t.c.rank_search))\
.limit(50)\ .limit(50)\
.subquery() .subquery()
sql = _select_from_placex(inner)\ sql = _select_from_placex(inner, False)\
.where(sa.or_(not inner.c.geometry.is_area(), .where(sa.or_(sa.not_(inner.c.geometry.is_area()),
inner.c.geometry.ST_Contains(wkt)))\ inner.c.geometry.ST_Contains(WKT_PARAM)))\
.order_by(sa.desc(inner.c.rank_search), inner.c.distance)\ .order_by(sa.desc(inner.c.rank_search), inner.c.distance)\
.limit(1) .limit(1)
sql = self._add_geometry_columns(sql, inner.c.geometry) sql = self._add_geometry_columns(sql, inner.c.geometry)
row = (await self.conn.execute(sql)).one_or_none() row = (await self.conn.execute(sql, self.bind_params)).one_or_none()
log().var_dump('Result (non-address feature)', row) log().var_dump('Result (non-address feature)', row)
return row return row
async def lookup_area(self, wkt: str) -> Optional[SaRow]: async def lookup_area(self) -> Optional[SaRow]:
""" Lookup large areas for the given WKT point. """ Lookup large areas for the current search.
""" """
log().section('Reverse lookup by larger area features') log().section('Reverse lookup by larger area features')
if self.layer_enabled(DataLayer.ADDRESS): if self.layer_enabled(DataLayer.ADDRESS):
address_row = await self._lookup_area_address(wkt) address_row = await self._lookup_area_address()
else: else:
address_row = None address_row = None
if self.has_feature_layers(): if self.has_feature_layers():
other_row = await self._lookup_area_others(wkt) other_row = await self._lookup_area_others()
else: else:
other_row = None other_row = None
return _get_closest(address_row, other_row) return _get_closest(address_row, other_row)
async def lookup_country(self, wkt: str) -> Optional[SaRow]: async def lookup_country(self) -> Optional[SaRow]:
""" Lookup the country for the given WKT point. """ Lookup the country for the current search.
""" """
log().section('Reverse lookup by country code') log().section('Reverse lookup by country code')
t = self.conn.t.country_grid t = self.conn.t.country_grid
sql = sa.select(t.c.country_code).distinct()\ sql = sa.select(t.c.country_code).distinct()\
.where(t.c.geometry.ST_Contains(wkt)) .where(t.c.geometry.ST_Contains(WKT_PARAM))
ccodes = tuple((r[0] for r in await self.conn.execute(sql))) ccodes = tuple((r[0] for r in await self.conn.execute(sql, self.bind_params)))
log().var_dump('Country codes', ccodes) log().var_dump('Country codes', ccodes)
if not ccodes: if not ccodes:
@@ -468,10 +475,10 @@ class ReverseGeocoder:
log().comment('Search for place nodes in country') log().comment('Search for place nodes in country')
inner = sa.select(t, inner = sa.select(t,
t.c.geometry.ST_Distance(wkt).label('distance'))\ t.c.geometry.ST_Distance(WKT_PARAM).label('distance'))\
.where(t.c.osm_type == 'N')\ .where(t.c.osm_type == 'N')\
.where(t.c.rank_search > 4)\ .where(t.c.rank_search > 4)\
.where(t.c.rank_search <= self.max_rank)\ .where(t.c.rank_search <= MAX_RANK_PARAM)\
.where(t.c.rank_address.between(5, 25))\ .where(t.c.rank_address.between(5, 25))\
.where(t.c.name != None)\ .where(t.c.name != None)\
.where(t.c.indexed_status == 0)\ .where(t.c.indexed_status == 0)\
@@ -480,26 +487,26 @@ class ReverseGeocoder:
.where(t.c.country_code.in_(ccodes))\ .where(t.c.country_code.in_(ccodes))\
.where(t.c.geometry .where(t.c.geometry
.ST_Buffer(sa.func.reverse_place_diameter(t.c.rank_search)) .ST_Buffer(sa.func.reverse_place_diameter(t.c.rank_search))
.intersects(wkt))\ .intersects(WKT_PARAM))\
.order_by(sa.desc(t.c.rank_search))\ .order_by(sa.desc(t.c.rank_search))\
.limit(50)\ .limit(50)\
.subquery() .subquery()
sql = _select_from_placex(inner)\ sql = _select_from_placex(inner, False)\
.where(inner.c.distance < sa.func.reverse_place_diameter(inner.c.rank_search))\ .where(inner.c.distance < sa.func.reverse_place_diameter(inner.c.rank_search))\
.order_by(sa.desc(inner.c.rank_search), inner.c.distance)\ .order_by(sa.desc(inner.c.rank_search), inner.c.distance)\
.limit(1) .limit(1)
sql = self._add_geometry_columns(sql, inner.c.geometry) sql = self._add_geometry_columns(sql, inner.c.geometry)
address_row = (await self.conn.execute(sql)).one_or_none() address_row = (await self.conn.execute(sql, self.bind_params)).one_or_none()
log().var_dump('Result (addressable place node)', address_row) log().var_dump('Result (addressable place node)', address_row)
else: else:
address_row = None address_row = None
if address_row is None: if address_row is None:
# Still nothing, then return a country with the appropriate country code. # Still nothing, then return a country with the appropriate country code.
sql = _select_from_placex(t, wkt)\ sql = _select_from_placex(t)\
.where(t.c.country_code.in_(ccodes))\ .where(t.c.country_code.in_(ccodes))\
.where(t.c.rank_address == 4)\ .where(t.c.rank_address == 4)\
.where(t.c.rank_search == 4)\ .where(t.c.rank_search == 4)\
@@ -509,7 +516,7 @@ class ReverseGeocoder:
sql = self._add_geometry_columns(sql, t.c.geometry) sql = self._add_geometry_columns(sql, t.c.geometry)
address_row = (await self.conn.execute(sql)).one_or_none() address_row = (await self.conn.execute(sql, self.bind_params)).one_or_none()
return address_row return address_row
@@ -521,26 +528,26 @@ class ReverseGeocoder:
log().function('reverse_lookup', coord=coord, params=self.params) log().function('reverse_lookup', coord=coord, params=self.params)
wkt = f'POINT({coord[0]} {coord[1]})' self.bind_params['wkt'] = f'SRID=4326;POINT({coord[0]} {coord[1]})'
row: Optional[SaRow] = None row: Optional[SaRow] = None
row_func: RowFunc = nres.create_from_placex_row row_func: RowFunc = nres.create_from_placex_row
if self.max_rank >= 26: if self.max_rank >= 26:
row, tmp_row_func = await self.lookup_street_poi(wkt) row, tmp_row_func = await self.lookup_street_poi()
if row is not None: if row is not None:
row_func = tmp_row_func row_func = tmp_row_func
if row is None and self.max_rank > 4: if row is None and self.max_rank > 4:
row = await self.lookup_area(wkt) row = await self.lookup_area()
if row is None and self.layer_enabled(DataLayer.ADDRESS): if row is None and self.layer_enabled(DataLayer.ADDRESS):
row = await self.lookup_country(wkt) row = await self.lookup_country()
result = row_func(row, nres.ReverseResult) result = row_func(row, nres.ReverseResult)
if result is not None: if result is not None:
assert row is not None assert row is not None
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)
await nres.add_result_details(self.conn, [result], self.params) await nres.add_result_details(self.conn, [result], self.params)
return result return result

View File

@@ -14,6 +14,7 @@ import dataclasses
import enum import enum
import math import math
from struct import unpack from struct import unpack
from binascii import unhexlify
import sqlalchemy as sa import sqlalchemy as sa
@@ -72,9 +73,11 @@ class Point(NamedTuple):
@staticmethod @staticmethod
def from_wkb(wkb: bytes) -> 'Point': def from_wkb(wkb: Union[str, bytes]) -> 'Point':
""" Create a point from EWKB as returned from the database. """ Create a point from EWKB as returned from the database.
""" """
if isinstance(wkb, str):
wkb = unhexlify(wkb)
if len(wkb) != 25: if len(wkb) != 25:
raise ValueError("Point wkb has unexpected length") raise ValueError("Point wkb has unexpected length")
if wkb[0] == 0: if wkb[0] == 0:
@@ -192,13 +195,16 @@ class Bbox:
@staticmethod @staticmethod
def from_wkb(wkb: Optional[bytes]) -> 'Optional[Bbox]': def from_wkb(wkb: Union[None, str, bytes]) -> 'Optional[Bbox]':
""" Create a Bbox from a bounding box polygon as returned by """ Create a Bbox from a bounding box polygon as returned by
the database. Return s None if the input value is None. the database. Return s None if the input value is None.
""" """
if wkb is None: if wkb is None:
return None return None
if isinstance(wkb, str):
wkb = unhexlify(wkb)
if len(wkb) != 97: if len(wkb) != 97:
raise ValueError("WKB must be a bounding box polygon") raise ValueError("WKB must be a bounding box polygon")
if wkb.startswith(WKB_BBOX_HEADER_LE): if wkb.startswith(WKB_BBOX_HEADER_LE):

View File

@@ -48,6 +48,9 @@ class Geometry(types.UserDefinedType[Any]):
class comparator_factory(types.UserDefinedType.Comparator): class comparator_factory(types.UserDefinedType.Comparator):
def intersects(self, other: SaColumn) -> SaColumn:
return self.op('&&')(other)
def is_line_like(self) -> SaColumn: def is_line_like(self) -> SaColumn:
return sa.func.ST_GeometryType(self, type_=sa.String).in_(('ST_LineString', return sa.func.ST_GeometryType(self, type_=sa.String).in_(('ST_LineString',
'ST_MultiLineString')) 'ST_MultiLineString'))

View File

@@ -222,3 +222,7 @@ NOMINATIM_LOG_DB=no
# Enable logging of requests into a file. # Enable logging of requests into a file.
# To enable logging set this setting to the file to log to. # To enable logging set this setting to the file to log to.
NOMINATIM_LOG_FILE= NOMINATIM_LOG_FILE=
# Echo raw SQL from SQLAlchemy statements.
# Works only in command line/library use.
NOMINATIM_DEBUG_SQL=no