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, Callable, Tuple, Type
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 RowFunc = Callable[[Optional[SaRow], Type[nres.BaseResultT]], Optional[nres.BaseResultT]]
23 def _select_column_geometry(column: SaColumn,
24 geometry_output: ntyp.GeometryFormat) -> SaLabel:
25 """ Create the appropriate column expression for selecting a
26 geometry for the details response.
28 if geometry_output & ntyp.GeometryFormat.GEOJSON:
29 return sa.literal_column(f"""
30 ST_AsGeoJSON(CASE WHEN ST_NPoints({column.name}) > 5000
31 THEN ST_SimplifyPreserveTopology({column.name}, 0.0001)
32 ELSE {column.name} END)
33 """).label('geometry_geojson')
35 return sa.func.ST_GeometryType(column).label('geometry_type')
38 async def find_in_placex(conn: SearchConnection, place: ntyp.PlaceRef,
39 details: ntyp.LookupDetails) -> Optional[SaRow]:
40 """ Search for the given place in the placex table and return the
43 log().section("Find in placex table")
45 sql = sa.select(t.c.place_id, t.c.osm_type, t.c.osm_id, t.c.name,
46 t.c.class_, t.c.type, t.c.admin_level,
47 t.c.address, t.c.extratags,
48 t.c.housenumber, t.c.postcode, t.c.country_code,
49 t.c.importance, t.c.wikipedia, t.c.indexed_date,
50 t.c.parent_place_id, t.c.rank_address, t.c.rank_search,
53 _select_column_geometry(t.c.geometry, details.geometry_output))
55 if isinstance(place, ntyp.PlaceID):
56 sql = sql.where(t.c.place_id == place.place_id)
57 elif isinstance(place, ntyp.OsmID):
58 sql = sql.where(t.c.osm_type == place.osm_type)\
59 .where(t.c.osm_id == place.osm_id)
61 sql = sql.where(t.c.class_ == place.osm_class)
63 sql = sql.order_by(t.c.class_)
68 return (await conn.execute(sql)).one_or_none()
71 async def find_in_osmline(conn: SearchConnection, place: ntyp.PlaceRef,
72 details: ntyp.LookupDetails) -> Optional[SaRow]:
73 """ Search for the given place in the osmline table and return the
76 log().section("Find in interpolation table")
78 sql = sa.select(t.c.place_id, t.c.osm_id, t.c.parent_place_id,
79 t.c.indexed_date, t.c.startnumber, t.c.endnumber,
80 t.c.step, t.c.address, t.c.postcode, t.c.country_code,
81 t.c.linegeo.ST_Centroid().label('centroid'),
82 _select_column_geometry(t.c.linegeo, details.geometry_output))
84 if isinstance(place, ntyp.PlaceID):
85 sql = sql.where(t.c.place_id == place.place_id)
86 elif isinstance(place, ntyp.OsmID) and place.osm_type == 'W':
87 # There may be multiple interpolations for a single way.
88 # If 'class' contains a number, return the one that belongs to that number.
89 sql = sql.where(t.c.osm_id == place.osm_id).limit(1)
90 if place.osm_class and place.osm_class.isdigit():
91 sql = sql.order_by(sa.func.greatest(0,
92 sa.func.least(int(place.osm_class) - t.c.endnumber),
93 t.c.startnumber - int(place.osm_class)))
97 return (await conn.execute(sql)).one_or_none()
100 async def find_in_tiger(conn: SearchConnection, place: ntyp.PlaceRef,
101 details: ntyp.LookupDetails) -> Optional[SaRow]:
102 """ Search for the given place in the table of Tiger addresses and return
103 the base information. Only lookup by place ID is supported.
105 log().section("Find in TIGER table")
107 parent = conn.t.placex
108 sql = sa.select(t.c.place_id, t.c.parent_place_id,
109 parent.c.osm_type, parent.c.osm_id,
110 t.c.startnumber, t.c.endnumber, t.c.step,
112 t.c.linegeo.ST_Centroid().label('centroid'),
113 _select_column_geometry(t.c.linegeo, details.geometry_output))
115 if isinstance(place, ntyp.PlaceID):
116 sql = sql.where(t.c.place_id == place.place_id)\
117 .join(parent, t.c.parent_place_id == parent.c.place_id, isouter=True)
121 return (await conn.execute(sql)).one_or_none()
124 async def find_in_postcode(conn: SearchConnection, place: ntyp.PlaceRef,
125 details: ntyp.LookupDetails) -> Optional[SaRow]:
126 """ Search for the given place in the postcode table and return the
127 base information. Only lookup by place ID is supported.
129 log().section("Find in postcode table")
131 sql = sa.select(t.c.place_id, t.c.parent_place_id,
132 t.c.rank_search, t.c.rank_address,
133 t.c.indexed_date, t.c.postcode, t.c.country_code,
134 t.c.geometry.label('centroid'),
135 _select_column_geometry(t.c.geometry, details.geometry_output))
137 if isinstance(place, ntyp.PlaceID):
138 sql = sql.where(t.c.place_id == place.place_id)
142 return (await conn.execute(sql)).one_or_none()
145 async def find_in_all_tables(conn: SearchConnection, place: ntyp.PlaceRef,
146 details: ntyp.LookupDetails
147 ) -> Tuple[Optional[SaRow], RowFunc[nres.BaseResultT]]:
148 """ Search for the given place in all data tables
149 and return the base information.
151 row = await find_in_placex(conn, place, details)
152 log().var_dump('Result (placex)', row)
154 return row, nres.create_from_placex_row
156 row = await find_in_osmline(conn, place, details)
157 log().var_dump('Result (osmline)', row)
159 return row, nres.create_from_osmline_row
161 row = await find_in_postcode(conn, place, details)
162 log().var_dump('Result (postcode)', row)
164 return row, nres.create_from_postcode_row
166 row = await find_in_tiger(conn, place, details)
167 log().var_dump('Result (tiger)', row)
168 return row, nres.create_from_tiger_row
171 async def get_detailed_place(conn: SearchConnection, place: ntyp.PlaceRef,
172 details: ntyp.LookupDetails) -> Optional[nres.DetailedResult]:
173 """ Retrieve a place with additional details from the database.
175 log().function('get_place_by_id', place=place, details=details)
177 if details.geometry_output and details.geometry_output != ntyp.GeometryFormat.GEOJSON:
178 raise ValueError("lookup only supports geojosn polygon output.")
180 row_func: RowFunc[nres.DetailedResult]
181 row, row_func = await find_in_all_tables(conn, place, details)
186 result = row_func(row, nres.DetailedResult)
187 assert result is not None
189 # add missing details
190 assert result is not None
191 result.parent_place_id = row.parent_place_id
192 result.linked_place_id = getattr(row, 'linked_place_id', None)
193 result.admin_level = getattr(row, 'admin_level', 15)
194 indexed_date = getattr(row, 'indexed_date', None)
195 if indexed_date is not None:
196 result.indexed_date = indexed_date.replace(tzinfo=dt.timezone.utc)
198 await nres.add_result_details(conn, result, details)