]> git.openstreetmap.org Git - nominatim.git/blobdiff - nominatim/api/reverse.py
introduce slim Geometry database type
[nominatim.git] / nominatim / api / reverse.py
index 42fe8f36af7deb613d3db15d3d850e05048e1a75..b8687b421548036d0aa756b94248da61368241bf 100644 (file)
@@ -10,13 +10,12 @@ Implementation of reverse geocoding.
 from typing import Optional, List, Callable, Type, Tuple
 
 import sqlalchemy as sa
 from typing import Optional, List, Callable, Type, Tuple
 
 import sqlalchemy as sa
-from geoalchemy2 import WKTElement
 
 from nominatim.typing import SaColumn, SaSelect, SaFromClause, SaLabel, SaRow
 from nominatim.api.connection import SearchConnection
 import nominatim.api.results as nres
 from nominatim.api.logging import log
 
 from nominatim.typing import SaColumn, SaSelect, SaFromClause, SaLabel, SaRow
 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, LookupDetails, GeometryFormat, Bbox
+from nominatim.api.types import AnyPoint, DataLayer, ReverseDetails, GeometryFormat, Bbox
 
 # In SQLAlchemy expression which compare with NULL need to be expressed with
 # the equal sign.
 
 # In SQLAlchemy expression which compare with NULL need to be expressed with
 # the equal sign.
@@ -33,11 +32,8 @@ def _select_from_placex(t: SaFromClause, wkt: Optional[str] = None) -> SaSelect:
         centroid = t.c.centroid
     else:
         distance = t.c.geometry.ST_Distance(wkt)
         centroid = t.c.centroid
     else:
         distance = t.c.geometry.ST_Distance(wkt)
-        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')
+        centroid = sa.case((t.c.geometry.is_line_like(), t.c.geometry.ST_ClosestPoint(wkt)),
+                           else_=t.c.centroid).label('centroid')
 
 
     return sa.select(t.c.place_id, t.c.osm_type, t.c.osm_id, t.c.name,
 
 
     return sa.select(t.c.place_id, t.c.osm_type, t.c.osm_id, t.c.name,
@@ -66,11 +62,10 @@ 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: WKTElement) -> SaLabel:
+def _locate_interpolation(table: SaFromClause, wkt: str) -> 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.ST_GeometryType() == 'ST_LineString',
-                    sa.func.ST_LineLocatePoint(table.c.linegeo, wkt)),
+    return sa.case((table.c.linegeo.is_line_like(), table.c.linegeo.ST_LineLocatePoint(wkt)),
                    else_=0).label('position')
 
 
                    else_=0).label('position')
 
 
@@ -87,23 +82,34 @@ class ReverseGeocoder:
         coordinate.
     """
 
         coordinate.
     """
 
-    def __init__(self, conn: SearchConnection, max_rank: int, layer: DataLayer,
-                 details: LookupDetails) -> None:
+    def __init__(self, conn: SearchConnection, params: ReverseDetails) -> None:
         self.conn = conn
         self.conn = conn
-        self.max_rank = max_rank
-        self.layer = layer
-        self.details = details
+        self.params = params
+
+
+    @property
+    def max_rank(self) -> int:
+        """ Return the maximum configured rank.
+        """
+        return self.params.max_rank
+
+
+    def has_geometries(self) -> bool:
+        """ Check if any geometries are requested.
+        """
+        return bool(self.params.geometry_output)
+
 
     def layer_enabled(self, *layer: DataLayer) -> bool:
         """ Return true when any of the given layer types are requested.
         """
 
     def layer_enabled(self, *layer: DataLayer) -> bool:
         """ Return true when any of the given layer types are requested.
         """
-        return any(self.layer & l for l in layer)
+        return any(self.params.layers & l for l in layer)
 
 
     def layer_disabled(self, *layer: DataLayer) -> bool:
         """ Return true when none of the given layer types is requested.
         """
 
 
     def layer_disabled(self, *layer: DataLayer) -> bool:
         """ Return true when none of the given layer types is requested.
         """
-        return not any(self.layer & l for l in layer)
+        return not any(self.params.layers & l for l in layer)
 
 
     def has_feature_layers(self) -> bool:
 
 
     def has_feature_layers(self) -> bool:
@@ -112,22 +118,22 @@ class ReverseGeocoder:
         return self.layer_enabled(DataLayer.RAILWAY, DataLayer.MANMADE, DataLayer.NATURAL)
 
     def _add_geometry_columns(self, sql: SaSelect, col: SaColumn) -> SaSelect:
         return self.layer_enabled(DataLayer.RAILWAY, DataLayer.MANMADE, DataLayer.NATURAL)
 
     def _add_geometry_columns(self, sql: SaSelect, col: SaColumn) -> SaSelect:
-        if not self.details.geometry_output:
+        if not self.has_geometries():
             return sql
 
         out = []
 
             return sql
 
         out = []
 
-        if self.details.geometry_simplification > 0.0:
-            col = col.ST_SimplifyPreserveTopology(self.details.geometry_simplification)
+        if self.params.geometry_simplification > 0.0:
+            col = sa.func.ST_SimplifyPreserveTopology(col, self.params.geometry_simplification)
 
 
-        if self.details.geometry_output & GeometryFormat.GEOJSON:
-            out.append(col.ST_AsGeoJSON().label('geometry_geojson'))
-        if self.details.geometry_output & GeometryFormat.TEXT:
-            out.append(col.ST_AsText().label('geometry_text'))
-        if self.details.geometry_output & GeometryFormat.KML:
-            out.append(col.ST_AsKML().label('geometry_kml'))
-        if self.details.geometry_output & GeometryFormat.SVG:
-            out.append(col.ST_AsSVG().label('geometry_svg'))
+        if self.params.geometry_output & GeometryFormat.GEOJSON:
+            out.append(sa.func.ST_AsGeoJSON(col).label('geometry_geojson'))
+        if self.params.geometry_output & GeometryFormat.TEXT:
+            out.append(sa.func.ST_AsText(col).label('geometry_text'))
+        if self.params.geometry_output & GeometryFormat.KML:
+            out.append(sa.func.ST_AsKML(col).label('geometry_kml'))
+        if self.params.geometry_output & GeometryFormat.SVG:
+            out.append(sa.func.ST_AsSVG(col).label('geometry_svg'))
 
         return sql.add_columns(*out)
 
 
         return sql.add_columns(*out)
 
@@ -149,7 +155,7 @@ 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: WKTElement,
+    async def _find_closest_street_or_poi(self, wkt: str,
                                           distance: float) -> Optional[SaRow]:
         """ Look up the closest rank 26+ place in the database, which
             is closer than the given distance.
                                           distance: float) -> Optional[SaRow]:
         """ Look up the closest rank 26+ place in the database, which
             is closer than the given distance.
@@ -160,8 +166,7 @@ class ReverseGeocoder:
                 .where(t.c.geometry.ST_DWithin(wkt, distance))\
                 .where(t.c.indexed_status == 0)\
                 .where(t.c.linked_place_id == None)\
                 .where(t.c.geometry.ST_DWithin(wkt, distance))\
                 .where(t.c.indexed_status == 0)\
                 .where(t.c.linked_place_id == None)\
-                .where(sa.or_(t.c.geometry.ST_GeometryType()
-                                          .not_in(('ST_Polygon', 'ST_MultiPolygon')),
+                .where(sa.or_(sa.not_(t.c.geometry.is_area()),
                               t.c.centroid.ST_Distance(wkt) < distance))\
                 .order_by('distance')\
                 .limit(1)
                               t.c.centroid.ST_Distance(wkt) < distance))\
                 .order_by('distance')\
                 .limit(1)
@@ -178,7 +183,7 @@ class ReverseGeocoder:
         if self.layer_enabled(DataLayer.POI) and self.max_rank == 30:
             restrict.append(sa.and_(t.c.rank_search == 30,
                                     t.c.class_.not_in(('place', 'building')),
         if self.layer_enabled(DataLayer.POI) and self.max_rank == 30:
             restrict.append(sa.and_(t.c.rank_search == 30,
                                     t.c.class_.not_in(('place', 'building')),
-                                    t.c.geometry.ST_GeometryType() != 'ST_LineString'))
+                                    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),
                                     t.c.rank_address == 0,
         if self.has_feature_layers():
             restrict.append(sa.and_(t.c.rank_search.between(26, self.max_rank),
                                     t.c.rank_address == 0,
@@ -191,7 +196,7 @@ class ReverseGeocoder:
 
 
     async def _find_housenumber_for_street(self, parent_place_id: int,
 
 
     async def _find_housenumber_for_street(self, parent_place_id: int,
-                                           wkt: WKTElement) -> Optional[SaRow]:
+                                           wkt: str) -> Optional[SaRow]:
         t = self.conn.t.placex
 
         sql = _select_from_placex(t, wkt)\
         t = self.conn.t.placex
 
         sql = _select_from_placex(t, wkt)\
@@ -209,7 +214,7 @@ class ReverseGeocoder:
 
 
     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: WKTElement,
+                                             wkt: str,
                                              distance: float) -> Optional[SaRow]:
         t = self.conn.t.osmline
 
                                              distance: float) -> Optional[SaRow]:
         t = self.conn.t.osmline
 
@@ -224,7 +229,7 @@ class ReverseGeocoder:
         if parent_place_id is not None:
             sql = sql.where(t.c.parent_place_id == parent_place_id)
 
         if parent_place_id is not None:
             sql = sql.where(t.c.parent_place_id == parent_place_id)
 
-        inner = sql.subquery()
+        inner = sql.subquery('ipol')
 
         sql = sa.select(inner.c.place_id, inner.c.osm_id,
                         inner.c.parent_place_id, inner.c.address,
 
         sql = sa.select(inner.c.place_id, inner.c.osm_id,
                         inner.c.parent_place_id, inner.c.address,
@@ -233,16 +238,16 @@ class ReverseGeocoder:
                         inner.c.postcode, inner.c.country_code,
                         inner.c.distance)
 
                         inner.c.postcode, inner.c.country_code,
                         inner.c.distance)
 
-        if self.details.geometry_output:
-            sub = sql.subquery()
-            sql = self._add_geometry_columns(sql, sub.c.centroid)
+        if self.has_geometries():
+            sub = sql.subquery('geom')
+            sql = self._add_geometry_columns(sa.select(sub), sub.c.centroid)
 
         return (await self.conn.execute(sql)).one_or_none()
 
 
     async def _find_tiger_number_for_street(self, parent_place_id: int,
                                             parent_type: str, parent_id: int,
 
         return (await self.conn.execute(sql)).one_or_none()
 
 
     async def _find_tiger_number_for_street(self, parent_place_id: int,
                                             parent_type: str, parent_id: int,
-                                            wkt: WKTElement) -> Optional[SaRow]:
+                                            wkt: str) -> Optional[SaRow]:
         t = self.conn.t.tiger
 
         inner = sa.select(t,
         t = self.conn.t.tiger
 
         inner = sa.select(t,
@@ -252,7 +257,7 @@ class ReverseGeocoder:
                   .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)\
-                  .subquery()
+                  .subquery('tiger')
 
         sql = sa.select(inner.c.place_id,
                         inner.c.parent_place_id,
 
         sql = sa.select(inner.c.place_id,
                         inner.c.parent_place_id,
@@ -263,15 +268,15 @@ class ReverseGeocoder:
                         inner.c.postcode,
                         inner.c.distance)
 
                         inner.c.postcode,
                         inner.c.distance)
 
-        if self.details.geometry_output:
-            sub = sql.subquery()
-            sql = self._add_geometry_columns(sql, sub.c.centroid)
+        if self.has_geometries():
+            sub = sql.subquery('geom')
+            sql = self._add_geometry_columns(sa.select(sub), sub.c.centroid)
 
         return (await self.conn.execute(sql)).one_or_none()
 
 
     async def lookup_street_poi(self,
 
         return (await self.conn.execute(sql)).one_or_none()
 
 
     async def lookup_street_poi(self,
-                                wkt: WKTElement) -> Tuple[Optional[SaRow], RowFunc]:
+                                wkt: str) -> Tuple[Optional[SaRow], RowFunc]:
         """ Find a street or POI/address for the given WKT point.
         """
         log().section('Reverse lookup on street/address level')
         """ Find a street or POI/address for the given WKT point.
         """
         log().section('Reverse lookup on street/address level')
@@ -326,7 +331,7 @@ class ReverseGeocoder:
         return row, row_func
 
 
         return row, row_func
 
 
-    async def _lookup_area_address(self, wkt: WKTElement) -> Optional[SaRow]:
+    async def _lookup_area_address(self, wkt: str) -> 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')
@@ -337,7 +342,7 @@ class ReverseGeocoder:
         inner = sa.select(t, sa.literal(0.0).label('distance'))\
                   .where(t.c.rank_search.between(5, self.max_rank))\
                   .where(t.c.rank_address.between(5, 25))\
         inner = sa.select(t, sa.literal(0.0).label('distance'))\
                   .where(t.c.rank_search.between(5, self.max_rank))\
                   .where(t.c.rank_address.between(5, 25))\
-                  .where(t.c.geometry.ST_GeometryType().in_(('ST_Polygon', 'ST_MultiPolygon')))\
+                  .where(t.c.geometry.is_area())\
                   .where(t.c.geometry.intersects(wkt))\
                   .where(t.c.name != None)\
                   .where(t.c.indexed_status == 0)\
                   .where(t.c.geometry.intersects(wkt))\
                   .where(t.c.name != None)\
                   .where(t.c.indexed_status == 0)\
@@ -345,7 +350,7 @@ class ReverseGeocoder:
                   .where(t.c.type != 'postcode')\
                   .order_by(sa.desc(t.c.rank_search))\
                   .limit(50)\
                   .where(t.c.type != 'postcode')\
                   .order_by(sa.desc(t.c.rank_search))\
                   .limit(50)\
-                  .subquery()
+                  .subquery('area')
 
         sql = _select_from_placex(inner)\
                   .where(inner.c.geometry.ST_Contains(wkt))\
 
         sql = _select_from_placex(inner)\
                   .where(inner.c.geometry.ST_Contains(wkt))\
@@ -374,7 +379,7 @@ class ReverseGeocoder:
                                 .intersects(wkt))\
                       .order_by(sa.desc(t.c.rank_search))\
                       .limit(50)\
                                 .intersects(wkt))\
                       .order_by(sa.desc(t.c.rank_search))\
                       .limit(50)\
-                      .subquery()
+                      .subquery('places')
 
             touter = self.conn.t.placex.alias('outer')
             sql = _select_from_placex(inner)\
 
             touter = self.conn.t.placex.alias('outer')
             sql = _select_from_placex(inner)\
@@ -395,7 +400,7 @@ class ReverseGeocoder:
         return address_row
 
 
         return address_row
 
 
-    async def _lookup_area_others(self, wkt: WKTElement) -> Optional[SaRow]:
+    async def _lookup_area_others(self, wkt: str) -> Optional[SaRow]:
         t = self.conn.t.placex
 
         inner = sa.select(t, t.c.geometry.ST_Distance(wkt).label('distance'))\
         t = self.conn.t.placex
 
         inner = sa.select(t, t.c.geometry.ST_Distance(wkt).label('distance'))\
@@ -413,8 +418,7 @@ class ReverseGeocoder:
                   .subquery()
 
         sql = _select_from_placex(inner)\
                   .subquery()
 
         sql = _select_from_placex(inner)\
-                  .where(sa.or_(inner.c.geometry.ST_GeometryType()
-                                                .not_in(('ST_Polygon', 'ST_MultiPolygon')),
+                  .where(sa.or_(not inner.c.geometry.is_area(),
                                 inner.c.geometry.ST_Contains(wkt)))\
                   .order_by(sa.desc(inner.c.rank_search), inner.c.distance)\
                   .limit(1)
                                 inner.c.geometry.ST_Contains(wkt)))\
                   .order_by(sa.desc(inner.c.rank_search), inner.c.distance)\
                   .limit(1)
@@ -427,7 +431,7 @@ class ReverseGeocoder:
         return row
 
 
         return row
 
 
-    async def lookup_area(self, wkt: WKTElement) -> Optional[SaRow]:
+    async def lookup_area(self, wkt: str) -> Optional[SaRow]:
         """ Lookup large areas for the given WKT point.
         """
         log().section('Reverse lookup by larger area features')
         """ Lookup large areas for the given WKT point.
         """
         log().section('Reverse lookup by larger area features')
@@ -445,7 +449,7 @@ class ReverseGeocoder:
         return _get_closest(address_row, other_row)
 
 
         return _get_closest(address_row, other_row)
 
 
-    async def lookup_country(self, wkt: WKTElement) -> Optional[SaRow]:
+    async def lookup_country(self, wkt: str) -> Optional[SaRow]:
         """ Lookup the country for the given WKT point.
         """
         log().section('Reverse lookup by country code')
         """ Lookup the country for the given WKT point.
         """
         log().section('Reverse lookup by country code')
@@ -514,12 +518,10 @@ class ReverseGeocoder:
         """ Look up a single coordinate. Returns the place information,
             if a place was found near the coordinates or None otherwise.
         """
         """ Look up a single coordinate. Returns the place information,
             if a place was found near the coordinates or None otherwise.
         """
-        log().function('reverse_lookup',
-                       coord=coord, max_rank=self.max_rank,
-                       layer=self.layer, details=self.details)
+        log().function('reverse_lookup', coord=coord, params=self.params)
 
 
 
 
-        wkt = WKTElement(f'POINT({coord[0]} {coord[1]})', srid=4326)
+        wkt = f'POINT({coord[0]} {coord[1]})'
 
         row: Optional[SaRow] = None
         row_func: RowFunc = nres.create_from_placex_row
 
         row: Optional[SaRow] = None
         row_func: RowFunc = nres.create_from_placex_row
@@ -539,6 +541,6 @@ class ReverseGeocoder:
             result.distance = row.distance
             if hasattr(row, 'bbox'):
                 result.bbox = Bbox.from_wkb(row.bbox.data)
             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.params)
 
         return result
 
         return result