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, Any
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]]
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 create_from_placex_row(row: SaRow) -> SearchResult:
139 """ Construct a new SearchResult and add the data from the result row
140 from the placex table.
142 result = SearchResult(source_table=SourceTable.PLACEX,
143 place_id=row.place_id,
144 parent_place_id=row.parent_place_id,
145 linked_place_id=row.linked_place_id,
146 osm_object=(row.osm_type, row.osm_id),
147 category=(row.class_, row.type),
148 admin_level=row.admin_level,
151 extratags=row.extratags,
152 housenumber=row.housenumber,
153 postcode=row.postcode,
154 wikipedia=row.wikipedia,
155 rank_address=row.rank_address,
156 rank_search=row.rank_search,
157 importance=row.importance,
158 country_code=row.country_code,
159 indexed_date=getattr(row, 'indexed_date'),
160 centroid=Point(row.x, row.y))
162 result.geometry = {k[9:]: v for k, v in row._mapping.items() # pylint: disable=W0212
163 if k.startswith('geometry_')}
168 async def add_result_details(conn: SearchConnection, result: SearchResult,
169 details: LookupDetails) -> None:
170 """ Retrieve more details from the database according to the
171 parameters specified in 'details'.
173 if details.address_details:
174 await complete_address_details(conn, result)
175 if details.linked_places:
176 await complete_linked_places(conn, result)
177 if details.parented_places:
178 await complete_parented_places(conn, result)
180 await complete_keywords(conn, result)
183 def _result_row_to_address_row(row: SaRow) -> AddressLine:
184 """ Create a new AddressLine from the results of a datbase query.
186 extratags: Dict[str, str] = getattr(row, 'extratags', {})
187 if 'place_type' in row:
188 extratags['place_type'] = row.place_type
190 return AddressLine(place_id=row.place_id,
191 osm_object=(row.osm_type, row.osm_id),
192 category=(getattr(row, 'class'), row.type),
195 admin_level=row.admin_level,
196 fromarea=row.fromarea,
197 isaddress=getattr(row, 'isaddress', True),
198 rank_address=row.rank_address,
199 distance=row.distance)
202 async def complete_address_details(conn: SearchConnection, result: SearchResult) -> None:
203 """ Retrieve information about places that make up the address of the result.
206 if result.source_table in (SourceTable.TIGER, SourceTable.OSMLINE):
207 if result.housenumber is not None:
208 housenumber = int(result.housenumber)
209 elif result.extratags is not None and 'startnumber' in result.extratags:
210 # details requests do not come with a specific house number
211 housenumber = int(result.extratags['startnumber'])
213 sfn = sa.func.get_addressdata(result.place_id, housenumber)\
214 .table_valued( # type: ignore[no-untyped-call]
215 sa.column('place_id', type_=sa.Integer),
217 sa.column('osm_id', type_=sa.BigInteger),
218 sa.column('name', type_=conn.t.types.Composite),
219 'class', 'type', 'place_type',
220 sa.column('admin_level', type_=sa.Integer),
221 sa.column('fromarea', type_=sa.Boolean),
222 sa.column('isaddress', type_=sa.Boolean),
223 sa.column('rank_address', type_=sa.SmallInteger),
224 sa.column('distance', type_=sa.Float))
225 sql = sa.select(sfn).order_by(sa.column('rank_address').desc(),
226 sa.column('isaddress').desc())
228 result.address_rows = []
229 for row in await conn.execute(sql):
230 result.address_rows.append(_result_row_to_address_row(row))
232 # pylint: disable=consider-using-f-string
233 def _placex_select_address_row(conn: SearchConnection,
234 centroid: Point) -> SaSelect:
236 return sa.select(t.c.place_id, t.c.osm_type, t.c.osm_id, t.c.name,
237 t.c.class_.label('class'), t.c.type,
239 sa.literal_column("""ST_GeometryType(geometry) in
240 ('ST_Polygon','ST_MultiPolygon')""").label('fromarea'),
243 """ST_DistanceSpheroid(geometry, 'SRID=4326;POINT(%f %f)'::geometry,
244 'SPHEROID["WGS 84",6378137,298.257223563, AUTHORITY["EPSG","7030"]]')
245 """ % centroid).label('distance'))
248 async def complete_linked_places(conn: SearchConnection, result: SearchResult) -> None:
249 """ Retrieve information about places that link to the result.
251 result.linked_rows = []
252 if result.source_table != SourceTable.PLACEX:
255 sql = _placex_select_address_row(conn, result.centroid)\
256 .where(conn.t.placex.c.linked_place_id == result.place_id)
258 for row in await conn.execute(sql):
259 result.linked_rows.append(_result_row_to_address_row(row))
262 async def complete_keywords(conn: SearchConnection, result: SearchResult) -> None:
263 """ Retrieve information about the search terms used for this place.
265 t = conn.t.search_name
266 sql = sa.select(t.c.name_vector, t.c.nameaddress_vector)\
267 .where(t.c.place_id == result.place_id)
269 result.name_keywords = []
270 result.address_keywords = []
271 for name_tokens, address_tokens in await conn.execute(sql):
273 sel = sa.select(t.c.word_id, t.c.word_token, t.c.word)
275 for row in await conn.execute(sel.where(t.c.word_id == sa.any_(name_tokens))):
276 result.name_keywords.append(WordInfo(*row))
278 for row in await conn.execute(sel.where(t.c.word_id == sa.any_(address_tokens))):
279 result.address_keywords.append(WordInfo(*row))
282 async def complete_parented_places(conn: SearchConnection, result: SearchResult) -> None:
283 """ Retrieve information about places that the result provides the
286 result.parented_rows = []
287 if result.source_table != SourceTable.PLACEX:
290 sql = _placex_select_address_row(conn, result.centroid)\
291 .where(conn.t.placex.c.parent_place_id == result.place_id)\
292 .where(conn.t.placex.c.rank_search == 30)
294 for row in await conn.execute(sql):
295 result.parented_rows.append(_result_row_to_address_row(row))