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 Dataclasses for search results and helper functions to fill them.
10 Data classes are part of the public API while the functions are for
11 internal use only. That's why they are implemented as free-standing functions
12 instead of member functions.
14 from typing import Optional, Tuple, Dict, Sequence
19 import sqlalchemy as sa
21 from nominatim.typing import SaSelect, SaRow
22 from nominatim.api.types import Point, LookupDetails
23 from nominatim.api.connection import SearchConnection
25 # This file defines complex result data classes.
26 # pylint: disable=too-many-instance-attributes
28 class SourceTable(enum.Enum):
29 """ Enumeration of kinds of results.
38 @dataclasses.dataclass
40 """ Detailed information about a related place.
42 place_id: Optional[int]
43 osm_object: Optional[Tuple[str, int]]
44 category: Tuple[str, str]
46 extratags: Optional[Dict[str, str]]
48 admin_level: Optional[int]
55 AddressLines = Sequence[AddressLine]
58 @dataclasses.dataclass
60 """ Detailed information about a search term.
64 word: Optional[str] = None
67 WordInfos = Sequence[WordInfo]
70 @dataclasses.dataclass
72 """ Data class collecting all available information about a search result.
74 source_table: SourceTable
75 category: Tuple[str, str]
78 place_id : Optional[int] = None
79 parent_place_id: Optional[int] = None
80 linked_place_id: Optional[int] = None
81 osm_object: Optional[Tuple[str, int]] = None
84 names: Optional[Dict[str, str]] = None
85 address: Optional[Dict[str, str]] = None
86 extratags: Optional[Dict[str, str]] = None
88 housenumber: Optional[str] = None
89 postcode: Optional[str] = None
90 wikipedia: Optional[str] = None
92 rank_address: int = 30
94 importance: Optional[float] = None
96 country_code: Optional[str] = None
98 indexed_date: Optional[dt.datetime] = None
100 address_rows: Optional[AddressLines] = None
101 linked_rows: Optional[AddressLines] = None
102 parented_rows: Optional[AddressLines] = None
103 name_keywords: Optional[WordInfos] = None
104 address_keywords: Optional[WordInfos] = None
106 geometry: Dict[str, str] = dataclasses.field(default_factory=dict)
110 def lat(self) -> float:
111 """ Get the latitude (or y) of the center point of the place.
113 return self.centroid[1]
117 def lon(self) -> float:
118 """ Get the longitude (or x) of the center point of the place.
120 return self.centroid[0]
123 def calculated_importance(self) -> float:
124 """ Get a valid importance value. This is either the stored importance
125 of the value or an artificial value computed from the place's
128 return self.importance or (0.7500001 - (self.rank_search/40.0))
131 # pylint: disable=consider-using-f-string
132 def centroid_as_geojson(self) -> str:
133 """ Get the centroid in GeoJSON format.
135 return '{"type": "Point","coordinates": [%f, %f]}' % self.centroid
138 def _filter_geometries(row: SaRow) -> Dict[str, str]:
139 return {k[9:]: v for k, v in row._mapping.items() # pylint: disable=W0212
140 if k.startswith('geometry_')}
143 def create_from_placex_row(row: SaRow) -> SearchResult:
144 """ Construct a new SearchResult and add the data from the result row
145 from the placex table.
147 return SearchResult(source_table=SourceTable.PLACEX,
148 place_id=row.place_id,
149 parent_place_id=row.parent_place_id,
150 linked_place_id=row.linked_place_id,
151 osm_object=(row.osm_type, row.osm_id),
152 category=(row.class_, row.type),
153 admin_level=row.admin_level,
156 extratags=row.extratags,
157 housenumber=row.housenumber,
158 postcode=row.postcode,
159 wikipedia=row.wikipedia,
160 rank_address=row.rank_address,
161 rank_search=row.rank_search,
162 importance=row.importance,
163 country_code=row.country_code,
164 indexed_date=getattr(row, 'indexed_date'),
165 centroid=Point(row.x, row.y),
166 geometry=_filter_geometries(row))
169 def create_from_osmline_row(row: SaRow) -> SearchResult:
170 """ Construct a new SearchResult and add the data from the result row
171 from the osmline table.
173 return SearchResult(source_table=SourceTable.OSMLINE,
174 place_id=row.place_id,
175 parent_place_id=row.parent_place_id,
176 osm_object=('W', row.osm_id),
177 category=('place', 'houses'),
179 postcode=row.postcode,
180 extratags={'startnumber': str(row.startnumber),
181 'endnumber': str(row.endnumber),
182 'step': str(row.step)},
183 country_code=row.country_code,
184 indexed_date=getattr(row, 'indexed_date'),
185 centroid=Point(row.x, row.y),
186 geometry=_filter_geometries(row))
189 def create_from_tiger_row(row: SaRow) -> SearchResult:
190 """ Construct a new SearchResult and add the data from the result row
191 from the Tiger table.
193 return SearchResult(source_table=SourceTable.TIGER,
194 place_id=row.place_id,
195 parent_place_id=row.parent_place_id,
196 category=('place', 'houses'),
197 postcode=row.postcode,
198 extratags={'startnumber': str(row.startnumber),
199 'endnumber': str(row.endnumber),
200 'step': str(row.step)},
202 centroid=Point(row.x, row.y),
203 geometry=_filter_geometries(row))
206 def create_from_postcode_row(row: SaRow) -> SearchResult:
207 """ Construct a new SearchResult and add the data from the result row
208 from the postcode centroid table.
210 return SearchResult(source_table=SourceTable.POSTCODE,
211 place_id=row.place_id,
212 parent_place_id=row.parent_place_id,
213 category=('place', 'postcode'),
214 names={'ref': row.postcode},
215 rank_search=row.rank_search,
216 rank_address=row.rank_address,
217 country_code=row.country_code,
218 centroid=Point(row.x, row.y),
219 indexed_date=row.indexed_date,
220 geometry=_filter_geometries(row))
223 async def add_result_details(conn: SearchConnection, result: SearchResult,
224 details: LookupDetails) -> None:
225 """ Retrieve more details from the database according to the
226 parameters specified in 'details'.
228 if details.address_details:
229 await complete_address_details(conn, result)
230 if details.linked_places:
231 await complete_linked_places(conn, result)
232 if details.parented_places:
233 await complete_parented_places(conn, result)
235 await complete_keywords(conn, result)
238 def _result_row_to_address_row(row: SaRow) -> AddressLine:
239 """ Create a new AddressLine from the results of a datbase query.
241 extratags: Dict[str, str] = getattr(row, 'extratags', {})
242 if 'place_type' in row:
243 extratags['place_type'] = row.place_type
246 if getattr(row, 'housenumber', None) is not None:
249 names['housenumber'] = row.housenumber
251 return AddressLine(place_id=row.place_id,
252 osm_object=None if row.osm_type is None else (row.osm_type, row.osm_id),
253 category=(getattr(row, 'class'), row.type),
256 admin_level=row.admin_level,
257 fromarea=row.fromarea,
258 isaddress=getattr(row, 'isaddress', True),
259 rank_address=row.rank_address,
260 distance=row.distance)
263 async def complete_address_details(conn: SearchConnection, result: SearchResult) -> None:
264 """ Retrieve information about places that make up the address of the result.
267 if result.source_table in (SourceTable.TIGER, SourceTable.OSMLINE):
268 if result.housenumber is not None:
269 housenumber = int(result.housenumber)
270 elif result.extratags is not None and 'startnumber' in result.extratags:
271 # details requests do not come with a specific house number
272 housenumber = int(result.extratags['startnumber'])
274 sfn = sa.func.get_addressdata(result.place_id, housenumber)\
275 .table_valued( # type: ignore[no-untyped-call]
276 sa.column('place_id', type_=sa.Integer),
278 sa.column('osm_id', type_=sa.BigInteger),
279 sa.column('name', type_=conn.t.types.Composite),
280 'class', 'type', 'place_type',
281 sa.column('admin_level', type_=sa.Integer),
282 sa.column('fromarea', type_=sa.Boolean),
283 sa.column('isaddress', type_=sa.Boolean),
284 sa.column('rank_address', type_=sa.SmallInteger),
285 sa.column('distance', type_=sa.Float))
286 sql = sa.select(sfn).order_by(sa.column('rank_address').desc(),
287 sa.column('isaddress').desc())
289 result.address_rows = []
290 for row in await conn.execute(sql):
291 result.address_rows.append(_result_row_to_address_row(row))
293 # pylint: disable=consider-using-f-string
294 def _placex_select_address_row(conn: SearchConnection,
295 centroid: Point) -> SaSelect:
297 return sa.select(t.c.place_id, t.c.osm_type, t.c.osm_id, t.c.name,
298 t.c.class_.label('class'), t.c.type,
299 t.c.admin_level, t.c.housenumber,
300 sa.literal_column("""ST_GeometryType(geometry) in
301 ('ST_Polygon','ST_MultiPolygon')""").label('fromarea'),
304 """ST_DistanceSpheroid(geometry, 'SRID=4326;POINT(%f %f)'::geometry,
305 'SPHEROID["WGS 84",6378137,298.257223563, AUTHORITY["EPSG","7030"]]')
306 """ % centroid).label('distance'))
309 async def complete_linked_places(conn: SearchConnection, result: SearchResult) -> None:
310 """ Retrieve information about places that link to the result.
312 result.linked_rows = []
313 if result.source_table != SourceTable.PLACEX:
316 sql = _placex_select_address_row(conn, result.centroid)\
317 .where(conn.t.placex.c.linked_place_id == result.place_id)
319 for row in await conn.execute(sql):
320 result.linked_rows.append(_result_row_to_address_row(row))
323 async def complete_keywords(conn: SearchConnection, result: SearchResult) -> None:
324 """ Retrieve information about the search terms used for this place.
326 t = conn.t.search_name
327 sql = sa.select(t.c.name_vector, t.c.nameaddress_vector)\
328 .where(t.c.place_id == result.place_id)
330 result.name_keywords = []
331 result.address_keywords = []
332 for name_tokens, address_tokens in await conn.execute(sql):
334 sel = sa.select(t.c.word_id, t.c.word_token, t.c.word)
336 for row in await conn.execute(sel.where(t.c.word_id == sa.any_(name_tokens))):
337 result.name_keywords.append(WordInfo(*row))
339 for row in await conn.execute(sel.where(t.c.word_id == sa.any_(address_tokens))):
340 result.address_keywords.append(WordInfo(*row))
343 async def complete_parented_places(conn: SearchConnection, result: SearchResult) -> None:
344 """ Retrieve information about places that the result provides the
347 result.parented_rows = []
348 if result.source_table != SourceTable.PLACEX:
351 sql = _placex_select_address_row(conn, result.centroid)\
352 .where(conn.t.placex.c.parent_place_id == result.place_id)\
353 .where(conn.t.placex.c.rank_search == 30)
355 for row in await conn.execute(sql):
356 result.parented_rows.append(_result_row_to_address_row(row))