1 # SPDX-License-Identifier: GPL-3.0-or-later
 
   3 # This file is part of Nominatim. (https://nominatim.org)
 
   5 # Copyright (C) 2023 by the Nominatim developer community.
 
   6 # For a full list of authors see the git log.
 
   8 Implementation of place lookup by ID.
 
  10 from typing import Optional
 
  12 import sqlalchemy as sa
 
  14 from nominatim.typing import SaColumn, SaLabel, SaRow
 
  15 from nominatim.api.connection import SearchConnection
 
  16 import nominatim.api.types as ntyp
 
  17 import nominatim.api.results as nres
 
  18 from nominatim.api.logging import log
 
  20 def _select_column_geometry(column: SaColumn,
 
  21                             geometry_output: ntyp.GeometryFormat) -> SaLabel:
 
  22     """ Create the appropriate column expression for selecting a
 
  23         geometry for the details response.
 
  25     if geometry_output & ntyp.GeometryFormat.GEOJSON:
 
  26         return sa.literal_column(f"""
 
  27                   ST_AsGeoJSON(CASE WHEN ST_NPoints({column.name}) > 5000
 
  28                                THEN ST_SimplifyPreserveTopology({column.name}, 0.0001)
 
  29                                ELSE {column.name} END)
 
  30                   """).label('geometry_geojson')
 
  32     return sa.func.ST_GeometryType(column).label('geometry_type')
 
  35 async def find_in_placex(conn: SearchConnection, place: ntyp.PlaceRef,
 
  36                          details: ntyp.LookupDetails) -> Optional[SaRow]:
 
  37     """ Search for the given place in the placex table and return the
 
  40     log().section("Find in placex table")
 
  42     sql = sa.select(t.c.place_id, t.c.osm_type, t.c.osm_id, t.c.name,
 
  43                     t.c.class_, t.c.type, t.c.admin_level,
 
  44                     t.c.address, t.c.extratags,
 
  45                     t.c.housenumber, t.c.postcode, t.c.country_code,
 
  46                     t.c.importance, t.c.wikipedia, t.c.indexed_date,
 
  47                     t.c.parent_place_id, t.c.rank_address, t.c.rank_search,
 
  50                     _select_column_geometry(t.c.geometry, details.geometry_output))
 
  52     if isinstance(place, ntyp.PlaceID):
 
  53         sql = sql.where(t.c.place_id == place.place_id)
 
  54     elif isinstance(place, ntyp.OsmID):
 
  55         sql = sql.where(t.c.osm_type == place.osm_type)\
 
  56                  .where(t.c.osm_id == place.osm_id)
 
  58             sql = sql.where(t.c.class_ == place.osm_class)
 
  60             sql = sql.order_by(t.c.class_)
 
  65     return (await conn.execute(sql)).one_or_none()
 
  68 async def find_in_osmline(conn: SearchConnection, place: ntyp.PlaceRef,
 
  69                           details: ntyp.LookupDetails) -> Optional[SaRow]:
 
  70     """ Search for the given place in the osmline table and return the
 
  73     log().section("Find in interpolation table")
 
  75     sql = sa.select(t.c.place_id, t.c.osm_id, t.c.parent_place_id,
 
  76                     t.c.indexed_date, t.c.startnumber, t.c.endnumber,
 
  77                     t.c.step, t.c.address, t.c.postcode, t.c.country_code,
 
  78                     t.c.linegeo.ST_Centroid().label('centroid'),
 
  79                     _select_column_geometry(t.c.linegeo, details.geometry_output))
 
  81     if isinstance(place, ntyp.PlaceID):
 
  82         sql = sql.where(t.c.place_id == place.place_id)
 
  83     elif isinstance(place, ntyp.OsmID) and place.osm_type == 'W':
 
  84         # There may be multiple interpolations for a single way.
 
  85         # If 'class' contains a number, return the one that belongs to that number.
 
  86         sql = sql.where(t.c.osm_id == place.osm_id).limit(1)
 
  87         if place.osm_class and place.osm_class.isdigit():
 
  88             sql = sql.order_by(sa.func.greatest(0,
 
  89                                     sa.func.least(int(place.osm_class) - t.c.endnumber),
 
  90                                            t.c.startnumber - int(place.osm_class)))
 
  94     return (await conn.execute(sql)).one_or_none()
 
  97 async def find_in_tiger(conn: SearchConnection, place: ntyp.PlaceRef,
 
  98                         details: ntyp.LookupDetails) -> Optional[SaRow]:
 
  99     """ Search for the given place in the table of Tiger addresses and return
 
 100         the base information. Only lookup by place ID is supported.
 
 102     log().section("Find in TIGER table")
 
 104     sql = sa.select(t.c.place_id, t.c.parent_place_id,
 
 105                     t.c.startnumber, t.c.endnumber, t.c.step,
 
 107                     t.c.linegeo.ST_Centroid().label('centroid'),
 
 108                     _select_column_geometry(t.c.linegeo, details.geometry_output))
 
 110     if isinstance(place, ntyp.PlaceID):
 
 111         sql = sql.where(t.c.place_id == place.place_id)
 
 115     return (await conn.execute(sql)).one_or_none()
 
 118 async def find_in_postcode(conn: SearchConnection, place: ntyp.PlaceRef,
 
 119                            details: ntyp.LookupDetails) -> Optional[SaRow]:
 
 120     """ Search for the given place in the postcode table and return the
 
 121         base information. Only lookup by place ID is supported.
 
 123     log().section("Find in postcode table")
 
 125     sql = sa.select(t.c.place_id, t.c.parent_place_id,
 
 126                     t.c.rank_search, t.c.rank_address,
 
 127                     t.c.indexed_date, t.c.postcode, t.c.country_code,
 
 128                     t.c.geometry.label('centroid'),
 
 129                     _select_column_geometry(t.c.geometry, details.geometry_output))
 
 131     if isinstance(place, ntyp.PlaceID):
 
 132         sql = sql.where(t.c.place_id == place.place_id)
 
 136     return (await conn.execute(sql)).one_or_none()
 
 139 async def get_place_by_id(conn: SearchConnection, place: ntyp.PlaceRef,
 
 140                           details: ntyp.LookupDetails) -> Optional[nres.SearchResult]:
 
 141     """ Retrieve a place with additional details from the database.
 
 143     log().function('get_place_by_id', place=place, details=details)
 
 145     if details.geometry_output and details.geometry_output != ntyp.GeometryFormat.GEOJSON:
 
 146         raise ValueError("lookup only supports geojosn polygon output.")
 
 148     row = await find_in_placex(conn, place, details)
 
 150         result = nres.create_from_placex_row(row)
 
 151         log().var_dump('Result', result)
 
 152         await nres.add_result_details(conn, result, details)
 
 155     row = await find_in_osmline(conn, place, details)
 
 157         result = nres.create_from_osmline_row(row)
 
 158         log().var_dump('Result', result)
 
 159         await nres.add_result_details(conn, result, details)
 
 162     row = await find_in_postcode(conn, place, details)
 
 164         result = nres.create_from_postcode_row(row)
 
 165         log().var_dump('Result', result)
 
 166         await nres.add_result_details(conn, result, details)
 
 169     row = await find_in_tiger(conn, place, details)
 
 171         result = nres.create_from_tiger_row(row)
 
 172         log().var_dump('Result', result)
 
 173         await nres.add_result_details(conn, result, details)
 
 176     # Nothing found under this ID.