]> git.openstreetmap.org Git - nominatim.git/commitdiff
switch reverse() to new Geometry datatype
authorSarah Hoffmann <lonvia@denofr.de>
Sun, 25 Jun 2023 12:02:00 +0000 (14:02 +0200)
committerSarah Hoffmann <lonvia@denofr.de>
Sat, 1 Jul 2023 16:03:07 +0000 (18:03 +0200)
Also switches to using bind parameters for recurring parameters.

nominatim/api/connection.py
nominatim/api/core.py
nominatim/api/logging.py
nominatim/api/results.py
nominatim/api/reverse.py
nominatim/api/types.py
nominatim/db/sqlalchemy_types.py
settings/env.defaults

index 49dad6640fed204a8e40e41721a272d9082fdc0a..72cabf7814bd4d5402d4b99dada5ce720fd8c852 100644 (file)
@@ -38,7 +38,7 @@ class SearchConnection:
                     ) -> Any:
         """ Execute a 'scalar()' query on the connection.
         """
                     ) -> Any:
         """ 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]':
         """ Execute a 'execute()' query on the connection.
         """
                      ) -> 'sa.Result[Any]':
         """ 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)
 
 
index a9fc12439364415e8a8039f2f45b1eccbea9756e..8d503fa5e836cbb9b82ab992530de5cead00bf4b 100644 (file)
@@ -66,7 +66,8 @@ class NominatimAPIAsync:
                        username=dsn.get('user'), password=dsn.get('password'),
                        host=dsn.get('host'), port=int(dsn['port']) if 'port' in dsn else None,
                        query=query)
                        username=dsn.get('user'), password=dsn.get('password'),
                        host=dsn.get('host'), port=int(dsn['port']) if 'port' in dsn else None,
                        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:
                 async with engine.begin() as conn:
 
             try:
                 async with engine.begin() as conn:
index 6c8b1b388224f8a787977b15e7c2e2fce5aaab03..8d9af10c81aceb57a2117a99ff28491b79c69665 100644 (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
 import datetime as dt
 import textwrap
 from contextvars import ContextVar
 import datetime as dt
 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:
-            return str(cast('sa.ClauseElement', statement)
-                         .compile(conn.sync_engine, compile_kwargs={"literal_binds": True}))
-        except sa.exc.CompileError:
-            pass
-        except NotImplementedError:
-            pass
+        compiled = cast('sa.ClauseElement', statement).compile(conn.sync_engine)
 
 
-        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:
             sqlstr = highlight(sqlstr, PostgresLexer(),
                                HtmlFormatter(nowrap=True, lineseparator='<br />'))
         if CODE_HIGHLIGHT:
             sqlstr = highlight(sqlstr, PostgresLexer(),
                                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:
-        sqlstr = '\n| '.join(textwrap.wrap(self.format_sql(conn, statement), width=78))
+    def sql(self, conn: AsyncConnection, statement: 'sa.Executable',
+            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")
 
 
index c661b508bd88ea9a4bb6793e750999949905d1f2..3416fc7a0d7196777c8649d99493469603cfba8f 100644 (file)
@@ -263,7 +263,7 @@ def create_from_placex_row(row: Optional[SaRow],
                       rank_search=row.rank_search,
                       importance=row.importance,
                       country_code=row.country_code,
                       rank_search=row.rank_search,
                       importance=row.importance,
                       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,
                      postcode=row.postcode,
                      country_code=row.country_code,
                      address=row.address,
                      postcode=row.postcode,
                      country_code=row.country_code,
-                     centroid=Point.from_wkb(row.centroid.data),
+                     centroid=Point.from_wkb(row.centroid),
                      geometry=_filter_geometries(row))
 
     if hnr is None:
                      geometry=_filter_geometries(row))
 
     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'),
                      postcode=row.postcode,
                      country_code='us',
                      category=('place', 'houses' if hnr is None else 'house'),
                      postcode=row.postcode,
                      country_code='us',
-                     centroid=Point.from_wkb(row.centroid.data),
+                     centroid=Point.from_wkb(row.centroid),
                      geometry=_filter_geometries(row))
 
     if hnr is None:
                      geometry=_filter_geometries(row))
 
     if hnr is None:
@@ -350,7 +350,7 @@ def create_from_postcode_row(row: Optional[SaRow],
                       rank_search=row.rank_search,
                       rank_address=row.rank_address,
                       country_code=row.country_code,
                       rank_search=row.rank_search,
                       rank_address=row.rank_address,
                       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,
                       category=('place', 'country'),
 
     return class_type(source_table=SourceTable.COUNTRY,
                       category=('place', 'country'),
-                      centroid=Point.from_wkb(row.centroid.data),
+                      centroid=Point.from_wkb(row.centroid),
                       names=row.name,
                       rank_address=4, rank_search=4,
                       country_code=row.country_code)
                       names=row.name,
                       rank_address=4, rank_search=4,
                       country_code=row.country_code)
index b8687b421548036d0aa756b94248da61368241bf..eda4a7a907e2f7db16a5efc9c075ef9ec4611a72 100644 (file)
@@ -16,6 +16,7 @@ from nominatim.api.connection import SearchConnection
 import nominatim.api.results as nres
 from nominatim.api.logging import log
 from nominatim.api.types import AnyPoint, DataLayer, ReverseDetails, GeometryFormat, Bbox
 import nominatim.api.results as nres
 from nominatim.api.logging import log
 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
 # the equal sign.
 
 # In SQLAlchemy expression which compare with NULL need to be expressed with
 # 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
         results.
     """
     """ Create a select statement with the columns relevant for reverse
         results.
     """
-    if wkt is None:
+    if not use_wkt:
         distance = t.c.distance
         centroid = t.c.centroid
     else:
         distance = t.c.distance
         centroid = t.c.centroid
     else:
-        distance = t.c.geometry.ST_Distance(wkt)
-        centroid = sa.case((t.c.geometry.is_line_like(), t.c.geometry.ST_ClosestPoint(wkt)),
+        distance = t.c.geometry.ST_Distance(WKT_PARAM)
+        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,
                           table.c.name.has_key('housename')))
 
                    sa.or_(table.c.housenumber != None,
                           table.c.name.has_key('housename')))
 
+
 def _get_closest(*rows: Optional[SaRow]) -> Optional[SaRow]:
     return min(rows, key=lambda row: 1000 if row is None else row.distance)
 
 def _get_closest(*rows: Optional[SaRow]) -> Optional[SaRow]:
     return min(rows, key=lambda row: 1000 if row is None else row.distance)
 
+
 class ReverseGeocoder:
     """ Class implementing the logic for looking up a place from a
         coordinate.
 class ReverseGeocoder:
     """ Class implementing the logic for looking up a place from a
         coordinate.
@@ -86,6 +93,8 @@ class ReverseGeocoder:
         self.conn = conn
         self.params = params
 
         self.conn = conn
         self.params = params
 
+        self.bind_params = {'max_rank': params.max_rank}
+
 
     @property
     def max_rank(self) -> int:
 
     @property
     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:
         if not self.has_geometries():
             return sql
     def _add_geometry_columns(self, sql: SaSelect, col: SaColumn) -> SaSelect:
         if not self.has_geometries():
             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,
-                                          distance: float) -> Optional[SaRow]:
+    async def _find_closest_street_or_poi(self, distance: float) -> Optional[SaRow]:
         """ Look up the closest rank 26+ place in the database, which
             is closer than the given distance.
         """
         t = self.conn.t.placex
 
         """ Look up the closest rank 26+ place in the database, which
             is closer than the given distance.
         """
         t = self.conn.t.placex
 
-        sql = _select_from_placex(t, wkt)\
-                .where(t.c.geometry.ST_DWithin(wkt, distance))\
+        sql = _select_from_placex(t)\
+                .where(t.c.geometry.ST_DWithin(WKT_PARAM, distance))\
                 .where(t.c.indexed_status == 0)\
                 .where(t.c.linked_place_id == None)\
                 .where(sa.or_(sa.not_(t.c.geometry.is_area()),
                 .where(t.c.indexed_status == 0)\
                 .where(t.c.linked_place_id == None)\
                 .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')\
                 .limit(1)
 
                 .order_by('distance')\
                 .limit(1)
 
@@ -185,22 +194,23 @@ class ReverseGeocoder:
                                     t.c.class_.not_in(('place', 'building')),
                                     sa.not_(t.c.geometry.is_line_like())))
         if self.has_feature_layers():
                                     t.c.class_.not_in(('place', 'building')),
                                     sa.not_(t.c.geometry.is_line_like())))
         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,
                                     self._filter_by_layer(t)))
 
         if not restrict:
             return None
 
                                     t.c.rank_address == 0,
                                     self._filter_by_layer(t)))
 
         if not restrict:
             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,
-                                           wkt: str) -> Optional[SaRow]:
+    async def _find_housenumber_for_street(self, parent_place_id: int) -> Optional[SaRow]:
         t = self.conn.t.placex
 
         t = self.conn.t.placex
 
-        sql = _select_from_placex(t, wkt)\
-                .where(t.c.geometry.ST_DWithin(wkt, 0.001))\
+        sql = _select_from_placex(t)\
+                .where(t.c.geometry.ST_DWithin(WKT_PARAM, 0.001))\
                 .where(t.c.parent_place_id == parent_place_id)\
                 .where(_is_address_point(t))\
                 .where(t.c.indexed_status == 0)\
                 .where(t.c.parent_place_id == parent_place_id)\
                 .where(_is_address_point(t))\
                 .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]:
         t = self.conn.t.osmline
 
         sql = sa.select(t,
                                              distance: float) -> Optional[SaRow]:
         t = self.conn.t.osmline
 
         sql = sa.select(t,
-                        t.c.linegeo.ST_Distance(wkt).label('distance'),
-                        _locate_interpolation(t, wkt))\
-                .where(t.c.linegeo.ST_DWithin(wkt, distance))\
+                        t.c.linegeo.ST_Distance(WKT_PARAM).label('distance'),
+                        _locate_interpolation(t))\
+                .where(t.c.linegeo.ST_DWithin(WKT_PARAM, distance))\
                 .where(t.c.startnumber != None)\
                 .order_by('distance')\
                 .limit(1)
                 .where(t.c.startnumber != None)\
                 .order_by('distance')\
                 .limit(1)
@@ -242,18 +251,18 @@ class ReverseGeocoder:
             sub = sql.subquery('geom')
             sql = self._add_geometry_columns(sa.select(sub), sub.c.centroid)
 
             sub = sql.subquery('geom')
             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,
-                                            wkt: str) -> Optional[SaRow]:
+                                            parent_type: str,
+                                            parent_id: int) -> Optional[SaRow]:
         t = self.conn.t.tiger
 
         inner = sa.select(t,
         t = self.conn.t.tiger
 
         inner = sa.select(t,
-                          t.c.linegeo.ST_Distance(wkt).label('distance'),
-                          _locate_interpolation(t, wkt))\
-                  .where(t.c.linegeo.ST_DWithin(wkt, 0.001))\
+                          t.c.linegeo.ST_Distance(WKT_PARAM).label('distance'),
+                          _locate_interpolation(t))\
+                  .where(t.c.linegeo.ST_DWithin(WKT_PARAM, 0.001))\
                   .where(t.c.parent_place_id == parent_place_id)\
                   .order_by('distance')\
                   .limit(1)\
                   .where(t.c.parent_place_id == parent_place_id)\
                   .order_by('distance')\
                   .limit(1)\
@@ -272,18 +281,17 @@ class ReverseGeocoder:
             sub = sql.subquery('geom')
             sql = self._add_geometry_columns(sa.select(sub), sub.c.centroid)
 
             sub = sql.subquery('geom')
             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,
-                                wkt: str) -> Tuple[Optional[SaRow], RowFunc]:
+    async def lookup_street_poi(self) -> Tuple[Optional[SaRow], RowFunc]:
         """ Find a street or POI/address for the given WKT point.
         """
         log().section('Reverse lookup on street/address level')
         distance = 0.006
         parent_place_id = None
 
         """ Find a street or POI/address for the given WKT point.
         """
         log().section('Reverse lookup on street/address level')
         distance = 0.006
         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
         log().var_dump('Result (street/building)', row)
 
         row_func: RowFunc = nres.create_from_placex_row
         log().var_dump('Result (street/building)', row)
 
@@ -296,7 +304,7 @@ class ReverseGeocoder:
                 distance = 0.001
                 parent_place_id = row.place_id
                 log().comment('Find housenumber for street')
                 distance = 0.001
                 parent_place_id = row.place_id
                 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)
 
                 if addr_row is not None:
                 log().var_dump('Result (street housenumber)', addr_row)
 
                 if addr_row is not None:
@@ -307,8 +315,7 @@ class ReverseGeocoder:
                     log().comment('Find TIGER housenumber for street')
                     addr_row = await self._find_tiger_number_for_street(parent_place_id,
                                                                         row.osm_type,
                     log().comment('Find TIGER housenumber for street')
                     addr_row = await self._find_tiger_number_for_street(parent_place_id,
                                                                         row.osm_type,
-                                                                        row.osm_id,
-                                                                        wkt)
+                                                                        row.osm_id)
                     log().var_dump('Result (street Tiger housenumber)', addr_row)
 
                     if addr_row is not None:
                     log().var_dump('Result (street Tiger housenumber)', addr_row)
 
                     if addr_row is not None:
@@ -322,7 +329,7 @@ class ReverseGeocoder:
         if self.max_rank > 27 and self.layer_enabled(DataLayer.ADDRESS):
             log().comment('Find interpolation for street')
             addr_row = await self._find_interpolation_for_street(parent_place_id,
         if self.max_rank > 27 and self.layer_enabled(DataLayer.ADDRESS):
             log().comment('Find interpolation for street')
             addr_row = await self._find_interpolation_for_street(parent_place_id,
-                                                                 wkt, distance)
+                                                                 distance)
             log().var_dump('Result (street interpolation)', addr_row)
             if addr_row is not None:
                 row = addr_row
             log().var_dump('Result (street interpolation)', addr_row)
             if addr_row is not None:
                 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.
         """
         log().comment('Reverse lookup by larger address area features')
         """ Lookup large addressable areas for the given WKT point.
         """
         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
         # later only a minimum of results needs to be checked with ST_Contains.
         inner = sa.select(t, sa.literal(0.0).label('distance'))\
         # The inner SQL brings results in the right order, so that
         # later only a minimum of results needs to be checked with ST_Contains.
         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.geometry.is_area())\
                   .where(t.c.rank_address.between(5, 25))\
                   .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.indexed_status == 0)\
                   .where(t.c.linked_place_id == None)\
                   .where(t.c.name != None)\
                   .where(t.c.indexed_status == 0)\
                   .where(t.c.linked_place_id == None)\
@@ -352,23 +359,23 @@ class ReverseGeocoder:
                   .limit(50)\
                   .subquery('area')
 
                   .limit(50)\
                   .subquery('area')
 
-        sql = _select_from_placex(inner)\
-                  .where(inner.c.geometry.ST_Contains(wkt))\
+        sql = _select_from_placex(inner, False)\
+                  .where(inner.c.geometry.ST_Contains(WKT_PARAM))\
                   .order_by(sa.desc(inner.c.rank_search))\
                   .limit(1)
 
         sql = self._add_geometry_columns(sql, inner.c.geometry)
 
                   .order_by(sa.desc(inner.c.rank_search))\
                   .limit(1)
 
         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)
 
         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')
             inner = sa.select(t,
         log().var_dump('Result (area)', address_row)
 
         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')
             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.rank_search > address_row.rank_search)\
                       .where(t.c.osm_type == 'N')\
                       .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.name != None)\
                       .where(t.c.indexed_status == 0)\
                       .where(t.c.rank_address.between(5, 25))\
                       .where(t.c.name != None)\
                       .where(t.c.indexed_status == 0)\
@@ -376,13 +383,13 @@ class ReverseGeocoder:
                       .where(t.c.type != 'postcode')\
                       .where(t.c.geometry
                                 .ST_Buffer(sa.func.reverse_place_diameter(t.c.rank_search))
                       .where(t.c.type != 'postcode')\
                       .where(t.c.geometry
                                 .ST_Buffer(sa.func.reverse_place_diameter(t.c.rank_search))
-                                .intersects(wkt))\
+                                .intersects(WKT_PARAM))\
                       .order_by(sa.desc(t.c.rank_search))\
                       .limit(50)\
                       .subquery('places')
 
             touter = self.conn.t.placex.alias('outer')
                       .order_by(sa.desc(t.c.rank_search))\
                       .limit(50)\
                       .subquery('places')
 
             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))\
                   .where(touter.c.place_id == address_row.place_id)\
                   .where(inner.c.distance < sa.func.reverse_place_diameter(inner.c.rank_search))\
                   .join(touter, touter.c.geometry.ST_Contains(inner.c.geometry))\
                   .where(touter.c.place_id == address_row.place_id)\
                   .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)
 
             if place_address_row is not None:
             log().var_dump('Result (place node)', place_address_row)
 
             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.indexed_status == 0)\
                   .where(t.c.linked_place_id == None)\
                   .where(self._filter_by_layer(t))\
                   .where(t.c.geometry
                                 .ST_Buffer(sa.func.reverse_place_diameter(t.c.rank_search))
                   .where(t.c.name != None)\
                   .where(t.c.indexed_status == 0)\
                   .where(t.c.linked_place_id == None)\
                   .where(self._filter_by_layer(t))\
                   .where(t.c.geometry
                                 .ST_Buffer(sa.func.reverse_place_diameter(t.c.rank_search))
-                                .intersects(wkt))\
+                                .intersects(WKT_PARAM))\
                   .order_by(sa.desc(t.c.rank_search))\
                   .limit(50)\
                   .subquery()
 
                   .order_by(sa.desc(t.c.rank_search))\
                   .limit(50)\
                   .subquery()
 
-        sql = _select_from_placex(inner)\
-                  .where(sa.or_(not inner.c.geometry.is_area(),
-                                inner.c.geometry.ST_Contains(wkt)))\
+        sql = _select_from_placex(inner, False)\
+                  .where(sa.or_(sa.not_(inner.c.geometry.is_area()),
+                                inner.c.geometry.ST_Contains(WKT_PARAM)))\
                   .order_by(sa.desc(inner.c.rank_search), inner.c.distance)\
                   .limit(1)
 
         sql = self._add_geometry_columns(sql, inner.c.geometry)
 
                   .order_by(sa.desc(inner.c.rank_search), inner.c.distance)\
                   .limit(1)
 
         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)
 
         return row
 
 
         log().var_dump('Result (non-address feature)', row)
 
         return row
 
 
-    async def lookup_area(self, wkt: str) -> Optional[SaRow]:
-        """ Lookup large areas for the given WKT point.
+    async def lookup_area(self) -> Optional[SaRow]:
+        """ Lookup large areas for the current search.
         """
         log().section('Reverse lookup by larger area features')
 
         if self.layer_enabled(DataLayer.ADDRESS):
         """
         log().section('Reverse lookup by larger area features')
 
         if self.layer_enabled(DataLayer.ADDRESS):
-            address_row = await self._lookup_area_address(wkt)
+            address_row = await self._lookup_area_address()
         else:
             address_row = None
 
         if self.has_feature_layers():
         else:
             address_row = None
 
         if self.has_feature_layers():
-            other_row = await self._lookup_area_others(wkt)
+            other_row = await self._lookup_area_others()
         else:
             other_row = None
 
         return _get_closest(address_row, other_row)
 
 
         else:
             other_row = None
 
         return _get_closest(address_row, other_row)
 
 
-    async def lookup_country(self, wkt: str) -> Optional[SaRow]:
-        """ Lookup the country for the given WKT point.
+    async def lookup_country(self) -> Optional[SaRow]:
+        """ Lookup the country for the current search.
         """
         log().section('Reverse lookup by country code')
         t = self.conn.t.country_grid
         sql = sa.select(t.c.country_code).distinct()\
         """
         log().section('Reverse lookup by country code')
         t = self.conn.t.country_grid
         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)
 
         if not ccodes:
         log().var_dump('Country codes', ccodes)
 
         if not ccodes:
@@ -468,10 +475,10 @@ class ReverseGeocoder:
             log().comment('Search for place nodes in country')
 
             inner = sa.select(t,
             log().comment('Search for place nodes in country')
 
             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.rank_search > 4)\
                       .where(t.c.osm_type == 'N')\
                       .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.name != None)\
                       .where(t.c.indexed_status == 0)\
                       .where(t.c.rank_address.between(5, 25))\
                       .where(t.c.name != None)\
                       .where(t.c.indexed_status == 0)\
@@ -480,26 +487,26 @@ class ReverseGeocoder:
                       .where(t.c.country_code.in_(ccodes))\
                       .where(t.c.geometry
                                 .ST_Buffer(sa.func.reverse_place_diameter(t.c.rank_search))
                       .where(t.c.country_code.in_(ccodes))\
                       .where(t.c.geometry
                                 .ST_Buffer(sa.func.reverse_place_diameter(t.c.rank_search))
-                                .intersects(wkt))\
+                                .intersects(WKT_PARAM))\
                       .order_by(sa.desc(t.c.rank_search))\
                       .limit(50)\
                       .subquery()
 
                       .order_by(sa.desc(t.c.rank_search))\
                       .limit(50)\
                       .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))\
                   .order_by(sa.desc(inner.c.rank_search), inner.c.distance)\
                   .limit(1)
 
             sql = self._add_geometry_columns(sql, inner.c.geometry)
 
                   .where(inner.c.distance < sa.func.reverse_place_diameter(inner.c.rank_search))\
                   .order_by(sa.desc(inner.c.rank_search), inner.c.distance)\
                   .limit(1)
 
             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)
         else:
             address_row = None
 
         if address_row is None:
             # Still nothing, then return a country with the appropriate country code.
             log().var_dump('Result (addressable place node)', address_row)
         else:
             address_row = None
 
         if address_row is None:
             # 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.rank_address == 4)\
                       .where(t.c.rank_search == 4)\
                       .where(t.c.country_code.in_(ccodes))\
                       .where(t.c.rank_address == 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_func: RowFunc = nres.create_from_placex_row
 
         if self.max_rank >= 26:
 
         row: Optional[SaRow] = None
         row_func: RowFunc = nres.create_from_placex_row
 
         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:
                 row_func = tmp_row_func
         if row is None and self.max_rank > 4:
             if row is not None:
                 row_func = tmp_row_func
         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)
         if result is not None:
             assert row is not None
             result.distance = row.distance
             if hasattr(row, 'bbox'):
 
         result = row_func(row, nres.ReverseResult)
         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)
+                result.bbox = Bbox.from_wkb(row.bbox)
             await nres.add_result_details(self.conn, [result], self.params)
 
         return result
             await nres.add_result_details(self.conn, [result], self.params)
 
         return result
index 9d19b75dde393e1fe5dc4f43544101945ffab213..7cb7b1334fc91304a6d13821d178209bcb71bf8f 100644 (file)
@@ -14,6 +14,7 @@ import dataclasses
 import enum
 import math
 from struct import unpack
 import enum
 import math
 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:
             raise ValueError("Point wkb has unexpected length")
         if wkb[0] == 0:
         if len(wkb) != 25:
             raise ValueError("Point wkb has unexpected length")
         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
             the database. Return s None if the input value is None.
         """
         if wkb is None:
             return None
 
         """ Create a Bbox from a bounding box polygon as returned by
             the database. Return s None if the input value is None.
         """
         if wkb is None:
             return None
 
+        if isinstance(wkb, str):
+            wkb = unhexlify(wkb)
+
         if len(wkb) != 97:
             raise ValueError("WKB must be a bounding box polygon")
         if wkb.startswith(WKB_BBOX_HEADER_LE):
         if len(wkb) != 97:
             raise ValueError("WKB must be a bounding box polygon")
         if wkb.startswith(WKB_BBOX_HEADER_LE):
index 88cae29f3a4e8982c60b32c33efc668ab88d7053..5131dad3fd5bb7e4b09621187a75e094c0417908 100644 (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:
             return sa.func.ST_GeometryType(self, type_=sa.String).in_(('ST_LineString',
                                                                        'ST_MultiLineString'))
         def is_line_like(self) -> SaColumn:
             return sa.func.ST_GeometryType(self, type_=sa.String).in_(('ST_LineString',
                                                                        'ST_MultiLineString'))
index 84cd24f16c5a162b3974e5f72d00705ee3c17efc..9c2f7cac18da3636b445663627dd400a7683848c 100644 (file)
@@ -222,3 +222,7 @@ NOMINATIM_LOG_DB=no
 # Enable logging of requests into a file.
 # To enable logging set this setting to the file to log to.
 NOMINATIM_LOG_FILE=
 # Enable logging of requests into a file.
 # To enable logging set this setting to the file to log to.
 NOMINATIM_LOG_FILE=
+
+# Echo raw SQL from SQLAlchemy statements.
+# Works only in command line/library use.
+NOMINATIM_DEBUG_SQL=no