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 parent = conn.t.placex
106 sql = sa.select(t.c.place_id, t.c.parent_place_id,
107 parent.c.osm_type, parent.c.osm_id,
108 t.c.startnumber, t.c.endnumber, t.c.step,
110 t.c.linegeo.ST_Centroid().label('centroid'),
111 _select_column_geometry(t.c.linegeo, details.geometry_output))
113 if isinstance(place, ntyp.PlaceID):
114 sql = sql.where(t.c.place_id == place.place_id)\
115 .join(parent, t.c.parent_place_id == parent.c.place_id, isouter=True)
119 return (await conn.execute(sql)).one_or_none()
122 async def find_in_postcode(conn: SearchConnection, place: ntyp.PlaceRef,
123 details: ntyp.LookupDetails) -> Optional[SaRow]:
124 """ Search for the given place in the postcode table and return the
125 base information. Only lookup by place ID is supported.
127 log().section("Find in postcode table")
129 sql = sa.select(t.c.place_id, t.c.parent_place_id,
130 t.c.rank_search, t.c.rank_address,
131 t.c.indexed_date, t.c.postcode, t.c.country_code,
132 t.c.geometry.label('centroid'),
133 _select_column_geometry(t.c.geometry, details.geometry_output))
135 if isinstance(place, ntyp.PlaceID):
136 sql = sql.where(t.c.place_id == place.place_id)
140 return (await conn.execute(sql)).one_or_none()
143 async def get_place_by_id(conn: SearchConnection, place: ntyp.PlaceRef,
144 details: ntyp.LookupDetails) -> Optional[nres.DetailedResult]:
145 """ Retrieve a place with additional details from the database.
147 log().function('get_place_by_id', place=place, details=details)
149 if details.geometry_output and details.geometry_output != ntyp.GeometryFormat.GEOJSON:
150 raise ValueError("lookup only supports geojosn polygon output.")
152 row = await find_in_placex(conn, place, details)
153 log().var_dump('Result (placex)', row)
155 result = nres.create_from_placex_row(row, nres.DetailedResult)
157 row = await find_in_osmline(conn, place, details)
158 log().var_dump('Result (osmline)', row)
160 result = nres.create_from_osmline_row(row, nres.DetailedResult)
162 row = await find_in_postcode(conn, place, details)
163 log().var_dump('Result (postcode)', row)
165 result = nres.create_from_postcode_row(row, nres.DetailedResult)
167 row = await find_in_tiger(conn, place, details)
168 log().var_dump('Result (tiger)', row)
170 result = nres.create_from_tiger_row(row, nres.DetailedResult)
174 # add missing details
175 assert result is not None
176 result.parent_place_id = row.parent_place_id
177 result.linked_place_id = getattr(row, 'linked_place_id', None)
178 result.admin_level = getattr(row, 'admin_level', 15)
179 indexed_date = getattr(row, 'indexed_date', None)
180 if indexed_date is not None:
181 result.indexed_date = indexed_date.replace(tzinfo=dt.timezone.utc)
183 await nres.add_result_details(conn, result, details)