]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/api/lookup.py
python: implement reverse lookup function
[nominatim.git] / nominatim / api / lookup.py
1 # SPDX-License-Identifier: GPL-3.0-or-later
2 #
3 # This file is part of Nominatim. (https://nominatim.org)
4 #
5 # Copyright (C) 2023 by the Nominatim developer community.
6 # For a full list of authors see the git log.
7 """
8 Implementation of place lookup by ID.
9 """
10 from typing import Optional
11 import datetime as dt
12
13 import sqlalchemy as sa
14
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
20
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.
25     """
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')
32
33     return sa.func.ST_GeometryType(column).label('geometry_type')
34
35
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
39         base information.
40     """
41     log().section("Find in placex table")
42     t = conn.t.placex
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,
49                     t.c.linked_place_id,
50                     t.c.centroid,
51                     _select_column_geometry(t.c.geometry, details.geometry_output))
52
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)
58         if place.osm_class:
59             sql = sql.where(t.c.class_ == place.osm_class)
60         else:
61             sql = sql.order_by(t.c.class_)
62         sql = sql.limit(1)
63     else:
64         return None
65
66     return (await conn.execute(sql)).one_or_none()
67
68
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
72         base information.
73     """
74     log().section("Find in interpolation table")
75     t = conn.t.osmline
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))
81
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)))
92     else:
93         return None
94
95     return (await conn.execute(sql)).one_or_none()
96
97
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.
102     """
103     log().section("Find in TIGER table")
104     t = conn.t.tiger
105     sql = sa.select(t.c.place_id, t.c.parent_place_id,
106                     t.c.startnumber, t.c.endnumber, t.c.step,
107                     t.c.postcode,
108                     t.c.linegeo.ST_Centroid().label('centroid'),
109                     _select_column_geometry(t.c.linegeo, details.geometry_output))
110
111     if isinstance(place, ntyp.PlaceID):
112         sql = sql.where(t.c.place_id == place.place_id)
113     else:
114         return None
115
116     return (await conn.execute(sql)).one_or_none()
117
118
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.
123     """
124     log().section("Find in postcode table")
125     t = conn.t.postcode
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))
131
132     if isinstance(place, ntyp.PlaceID):
133         sql = sql.where(t.c.place_id == place.place_id)
134     else:
135         return None
136
137     return (await conn.execute(sql)).one_or_none()
138
139
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.
143     """
144     log().function('get_place_by_id', place=place, details=details)
145
146     if details.geometry_output and details.geometry_output != ntyp.GeometryFormat.GEOJSON:
147         raise ValueError("lookup only supports geojosn polygon output.")
148
149     row = await find_in_placex(conn, place, details)
150     log().var_dump('Result (placex)', row)
151     if row is not None:
152         result = nres.create_from_placex_row(row, nres.DetailedResult)
153     else:
154         row = await find_in_osmline(conn, place, details)
155         log().var_dump('Result (osmline)', row)
156         if row is not None:
157             result = nres.create_from_osmline_row(row, nres.DetailedResult)
158         else:
159             row = await find_in_postcode(conn, place, details)
160             log().var_dump('Result (postcode)', row)
161             if row is not None:
162                 result = nres.create_from_postcode_row(row, nres.DetailedResult)
163             else:
164                 row = await find_in_tiger(conn, place, details)
165                 log().var_dump('Result (tiger)', row)
166                 if row is not None:
167                     result = nres.create_from_tiger_row(row, nres.DetailedResult)
168                 else:
169                     return None
170
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)
179
180     await nres.add_result_details(conn, result, details)
181
182     return result