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
13 import sqlalchemy as sa
15 from nominatim.typing import SaColumn, SaLabel, SaRow
16 from nominatim.api.connection import SearchConnection
17 import nominatim.api.types as ntyp
18 import nominatim.api.results as nres
19 from nominatim.api.logging import log
21 def _select_column_geometry(column: SaColumn,
22 geometry_output: ntyp.GeometryFormat) -> SaLabel:
23 """ Create the appropriate column expression for selecting a
24 geometry for the details response.
26 if geometry_output & ntyp.GeometryFormat.GEOJSON:
27 return sa.literal_column(f"""
28 ST_AsGeoJSON(CASE WHEN ST_NPoints({column.name}) > 5000
29 THEN ST_SimplifyPreserveTopology({column.name}, 0.0001)
30 ELSE {column.name} END)
31 """).label('geometry_geojson')
33 return sa.func.ST_GeometryType(column).label('geometry_type')
36 async def find_in_placex(conn: SearchConnection, place: ntyp.PlaceRef,
37 details: ntyp.LookupDetails) -> Optional[SaRow]:
38 """ Search for the given place in the placex table and return the
41 log().section("Find in placex table")
43 sql = sa.select(t.c.place_id, t.c.osm_type, t.c.osm_id, t.c.name,
44 t.c.class_, t.c.type, t.c.admin_level,
45 t.c.address, t.c.extratags,
46 t.c.housenumber, t.c.postcode, t.c.country_code,
47 t.c.importance, t.c.wikipedia, t.c.indexed_date,
48 t.c.parent_place_id, t.c.rank_address, t.c.rank_search,
51 _select_column_geometry(t.c.geometry, details.geometry_output))
53 if isinstance(place, ntyp.PlaceID):
54 sql = sql.where(t.c.place_id == place.place_id)
55 elif isinstance(place, ntyp.OsmID):
56 sql = sql.where(t.c.osm_type == place.osm_type)\
57 .where(t.c.osm_id == place.osm_id)
59 sql = sql.where(t.c.class_ == place.osm_class)
61 sql = sql.order_by(t.c.class_)
66 return (await conn.execute(sql)).one_or_none()
69 async def find_in_osmline(conn: SearchConnection, place: ntyp.PlaceRef,
70 details: ntyp.LookupDetails) -> Optional[SaRow]:
71 """ Search for the given place in the osmline table and return the
74 log().section("Find in interpolation table")
76 sql = sa.select(t.c.place_id, t.c.osm_id, t.c.parent_place_id,
77 t.c.indexed_date, t.c.startnumber, t.c.endnumber,
78 t.c.step, t.c.address, t.c.postcode, t.c.country_code,
79 t.c.linegeo.ST_Centroid().label('centroid'),
80 _select_column_geometry(t.c.linegeo, details.geometry_output))
82 if isinstance(place, ntyp.PlaceID):
83 sql = sql.where(t.c.place_id == place.place_id)
84 elif isinstance(place, ntyp.OsmID) and place.osm_type == 'W':
85 # There may be multiple interpolations for a single way.
86 # If 'class' contains a number, return the one that belongs to that number.
87 sql = sql.where(t.c.osm_id == place.osm_id).limit(1)
88 if place.osm_class and place.osm_class.isdigit():
89 sql = sql.order_by(sa.func.greatest(0,
90 sa.func.least(int(place.osm_class) - t.c.endnumber),
91 t.c.startnumber - int(place.osm_class)))
95 return (await conn.execute(sql)).one_or_none()
98 async def find_in_tiger(conn: SearchConnection, place: ntyp.PlaceRef,
99 details: ntyp.LookupDetails) -> Optional[SaRow]:
100 """ Search for the given place in the table of Tiger addresses and return
101 the base information. Only lookup by place ID is supported.
103 log().section("Find in TIGER table")
105 sql = sa.select(t.c.place_id, t.c.parent_place_id,
106 t.c.startnumber, t.c.endnumber, t.c.step,
108 t.c.linegeo.ST_Centroid().label('centroid'),
109 _select_column_geometry(t.c.linegeo, details.geometry_output))
111 if isinstance(place, ntyp.PlaceID):
112 sql = sql.where(t.c.place_id == place.place_id)
116 return (await conn.execute(sql)).one_or_none()
119 async def find_in_postcode(conn: SearchConnection, place: ntyp.PlaceRef,
120 details: ntyp.LookupDetails) -> Optional[SaRow]:
121 """ Search for the given place in the postcode table and return the
122 base information. Only lookup by place ID is supported.
124 log().section("Find in postcode table")
126 sql = sa.select(t.c.place_id, t.c.parent_place_id,
127 t.c.rank_search, t.c.rank_address,
128 t.c.indexed_date, t.c.postcode, t.c.country_code,
129 t.c.geometry.label('centroid'),
130 _select_column_geometry(t.c.geometry, details.geometry_output))
132 if isinstance(place, ntyp.PlaceID):
133 sql = sql.where(t.c.place_id == place.place_id)
137 return (await conn.execute(sql)).one_or_none()
140 async def get_place_by_id(conn: SearchConnection, place: ntyp.PlaceRef,
141 details: ntyp.LookupDetails) -> Optional[nres.DetailedResult]:
142 """ Retrieve a place with additional details from the database.
144 log().function('get_place_by_id', place=place, details=details)
146 if details.geometry_output and details.geometry_output != ntyp.GeometryFormat.GEOJSON:
147 raise ValueError("lookup only supports geojosn polygon output.")
149 row = await find_in_placex(conn, place, details)
150 log().var_dump('Result (placex)', row)
152 result = nres.create_from_placex_row(row, nres.DetailedResult)
154 row = await find_in_osmline(conn, place, details)
155 log().var_dump('Result (osmline)', row)
157 result = nres.create_from_osmline_row(row, nres.DetailedResult)
159 row = await find_in_postcode(conn, place, details)
160 log().var_dump('Result (postcode)', row)
162 result = nres.create_from_postcode_row(row, nres.DetailedResult)
164 row = await find_in_tiger(conn, place, details)
165 log().var_dump('Result (tiger)', row)
167 result = nres.create_from_tiger_row(row, nres.DetailedResult)
171 # add missing details
172 assert result is not None
173 result.parent_place_id = row.parent_place_id
174 result.linked_place_id = getattr(row, 'linked_place_id', None)
175 result.admin_level = getattr(row, 'admin_level', 15)
176 indexed_date = getattr(row, 'indexed_date', None)
177 if indexed_date is not None:
178 result.indexed_date = indexed_date.replace(tzinfo=dt.timezone.utc)
180 await nres.add_result_details(conn, result, details)