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.