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]]
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 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
191 if getattr(row, 'housenumber', None) is not None:
194 names['housenumber'] = row.housenumber
196 return AddressLine(place_id=row.place_id,
197 osm_object=None if row.osm_type is None else (row.osm_type, row.osm_id),
198 category=(getattr(row, 'class'), row.type),
201 admin_level=row.admin_level,
202 fromarea=row.fromarea,
203 isaddress=getattr(row, 'isaddress', True),
204 rank_address=row.rank_address,
205 distance=row.distance)
208 async def complete_address_details(conn: SearchConnection, result: SearchResult) -> None:
209 """ Retrieve information about places that make up the address of the result.
212 if result.source_table in (SourceTable.TIGER, SourceTable.OSMLINE):
213 if result.housenumber is not None:
214 housenumber = int(result.housenumber)
215 elif result.extratags is not None and 'startnumber' in result.extratags:
216 # details requests do not come with a specific house number
217 housenumber = int(result.extratags['startnumber'])
219 sfn = sa.func.get_addressdata(result.place_id, housenumber)\
220 .table_valued( # type: ignore[no-untyped-call]
221 sa.column('place_id', type_=sa.Integer),
223 sa.column('osm_id', type_=sa.BigInteger),
224 sa.column('name', type_=conn.t.types.Composite),
225 'class', 'type', 'place_type',
226 sa.column('admin_level', type_=sa.Integer),
227 sa.column('fromarea', type_=sa.Boolean),
228 sa.column('isaddress', type_=sa.Boolean),
229 sa.column('rank_address', type_=sa.SmallInteger),
230 sa.column('distance', type_=sa.Float))
231 sql = sa.select(sfn).order_by(sa.column('rank_address').desc(),
232 sa.column('isaddress').desc())
234 result.address_rows = []
235 for row in await conn.execute(sql):
236 result.address_rows.append(_result_row_to_address_row(row))
238 # pylint: disable=consider-using-f-string
239 def _placex_select_address_row(conn: SearchConnection,
240 centroid: Point) -> SaSelect:
242 return sa.select(t.c.place_id, t.c.osm_type, t.c.osm_id, t.c.name,
243 t.c.class_.label('class'), t.c.type,
244 t.c.admin_level, t.c.housenumber,
245 sa.literal_column("""ST_GeometryType(geometry) in
246 ('ST_Polygon','ST_MultiPolygon')""").label('fromarea'),
249 """ST_DistanceSpheroid(geometry, 'SRID=4326;POINT(%f %f)'::geometry,
250 'SPHEROID["WGS 84",6378137,298.257223563, AUTHORITY["EPSG","7030"]]')
251 """ % centroid).label('distance'))
254 async def complete_linked_places(conn: SearchConnection, result: SearchResult) -> None:
255 """ Retrieve information about places that link to the result.
257 result.linked_rows = []
258 if result.source_table != SourceTable.PLACEX:
261 sql = _placex_select_address_row(conn, result.centroid)\
262 .where(conn.t.placex.c.linked_place_id == result.place_id)
264 for row in await conn.execute(sql):
265 result.linked_rows.append(_result_row_to_address_row(row))
268 async def complete_keywords(conn: SearchConnection, result: SearchResult) -> None:
269 """ Retrieve information about the search terms used for this place.
271 t = conn.t.search_name
272 sql = sa.select(t.c.name_vector, t.c.nameaddress_vector)\
273 .where(t.c.place_id == result.place_id)
275 result.name_keywords = []
276 result.address_keywords = []
277 for name_tokens, address_tokens in await conn.execute(sql):
279 sel = sa.select(t.c.word_id, t.c.word_token, t.c.word)
281 for row in await conn.execute(sel.where(t.c.word_id == sa.any_(name_tokens))):
282 result.name_keywords.append(WordInfo(*row))
284 for row in await conn.execute(sel.where(t.c.word_id == sa.any_(address_tokens))):
285 result.address_keywords.append(WordInfo(*row))
288 async def complete_parented_places(conn: SearchConnection, result: SearchResult) -> None:
289 """ Retrieve information about places that the result provides the
292 result.parented_rows = []
293 if result.source_table != SourceTable.PLACEX:
296 sql = _placex_select_address_row(conn, result.centroid)\
297 .where(conn.t.placex.c.parent_place_id == result.place_id)\
298 .where(conn.t.placex.c.rank_search == 30)
300 for row in await conn.execute(sql):
301 result.parented_rows.append(_result_row_to_address_row(row))