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 the acutal database accesses for forward search.
10 from typing import List, Tuple, AsyncIterator, Dict, Any, Callable, cast
13 import sqlalchemy as sa
15 from nominatim.typing import SaFromClause, SaScalarSelect, SaColumn, \
16 SaExpression, SaSelect, SaLambdaSelect, SaRow, SaBind
17 from nominatim.api.connection import SearchConnection
18 from nominatim.api.types import SearchDetails, DataLayer, GeometryFormat, Bbox
19 import nominatim.api.results as nres
20 from nominatim.api.search.db_search_fields import SearchData, WeightedCategories
21 from nominatim.db.sqlalchemy_types import Geometry, IntArray
23 #pylint: disable=singleton-comparison,not-callable
24 #pylint: disable=too-many-branches,too-many-arguments,too-many-locals,too-many-statements
26 def no_index(expr: SaColumn) -> SaColumn:
27 """ Wrap the given expression, so that the query planner will
28 refrain from using the expression for index lookup.
30 return sa.func.coalesce(sa.null(), expr) # pylint: disable=not-callable
33 def _details_to_bind_params(details: SearchDetails) -> Dict[str, Any]:
34 """ Create a dictionary from search parameters that can be used
35 as bind parameter for SQL execute.
37 return {'limit': details.max_results,
38 'min_rank': details.min_rank,
39 'max_rank': details.max_rank,
40 'viewbox': details.viewbox,
41 'viewbox2': details.viewbox_x2,
43 'near_radius': details.near_radius,
44 'excluded': details.excluded,
45 'countries': details.countries}
48 LIMIT_PARAM: SaBind = sa.bindparam('limit')
49 MIN_RANK_PARAM: SaBind = sa.bindparam('min_rank')
50 MAX_RANK_PARAM: SaBind = sa.bindparam('max_rank')
51 VIEWBOX_PARAM: SaBind = sa.bindparam('viewbox', type_=Geometry)
52 VIEWBOX2_PARAM: SaBind = sa.bindparam('viewbox2', type_=Geometry)
53 NEAR_PARAM: SaBind = sa.bindparam('near', type_=Geometry)
54 NEAR_RADIUS_PARAM: SaBind = sa.bindparam('near_radius')
55 COUNTRIES_PARAM: SaBind = sa.bindparam('countries')
58 def filter_by_area(sql: SaSelect, t: SaFromClause,
59 details: SearchDetails, avoid_index: bool = False) -> SaSelect:
60 """ Apply SQL statements for filtering by viewbox and near point,
63 if details.near is not None and details.near_radius is not None:
64 if details.near_radius < 0.1 and not avoid_index:
65 sql = sql.where(t.c.geometry.within_distance(NEAR_PARAM, NEAR_RADIUS_PARAM))
67 sql = sql.where(t.c.geometry.ST_Distance(NEAR_PARAM) <= NEAR_RADIUS_PARAM)
68 if details.viewbox is not None and details.bounded_viewbox:
69 sql = sql.where(t.c.geometry.intersects(VIEWBOX_PARAM,
70 use_index=not avoid_index and
71 details.viewbox.area < 0.2))
76 def _exclude_places(t: SaFromClause) -> Callable[[], SaExpression]:
77 return lambda: t.c.place_id.not_in(sa.bindparam('excluded'))
80 def _select_placex(t: SaFromClause) -> SaSelect:
81 return sa.select(t.c.place_id, t.c.osm_type, t.c.osm_id, t.c.name,
83 t.c.address, t.c.extratags,
84 t.c.housenumber, t.c.postcode, t.c.country_code,
86 t.c.parent_place_id, t.c.rank_address, t.c.rank_search,
87 t.c.linked_place_id, t.c.admin_level,
89 t.c.geometry.ST_Expand(0).label('bbox'))
92 def _add_geometry_columns(sql: SaLambdaSelect, col: SaColumn, details: SearchDetails) -> SaSelect:
95 if details.geometry_simplification > 0.0:
96 col = sa.func.ST_SimplifyPreserveTopology(col, details.geometry_simplification)
98 if details.geometry_output & GeometryFormat.GEOJSON:
99 out.append(sa.func.ST_AsGeoJSON(col, 7).label('geometry_geojson'))
100 if details.geometry_output & GeometryFormat.TEXT:
101 out.append(sa.func.ST_AsText(col).label('geometry_text'))
102 if details.geometry_output & GeometryFormat.KML:
103 out.append(sa.func.ST_AsKML(col, 7).label('geometry_kml'))
104 if details.geometry_output & GeometryFormat.SVG:
105 out.append(sa.func.ST_AsSVG(col, 0, 7).label('geometry_svg'))
107 return sql.add_columns(*out)
110 def _make_interpolation_subquery(table: SaFromClause, inner: SaFromClause,
111 numerals: List[int], details: SearchDetails) -> SaScalarSelect:
112 all_ids = sa.func.ArrayAgg(table.c.place_id)
113 sql = sa.select(all_ids).where(table.c.parent_place_id == inner.c.place_id)
115 if len(numerals) == 1:
116 sql = sql.where(sa.between(numerals[0], table.c.startnumber, table.c.endnumber))\
117 .where((numerals[0] - table.c.startnumber) % table.c.step == 0)
119 sql = sql.where(sa.or_(
120 *(sa.and_(sa.between(n, table.c.startnumber, table.c.endnumber),
121 (n - table.c.startnumber) % table.c.step == 0)
125 sql = sql.where(_exclude_places(table))
127 return sql.scalar_subquery()
130 def _filter_by_layer(table: SaFromClause, layers: DataLayer) -> SaColumn:
131 orexpr: List[SaExpression] = []
132 if layers & DataLayer.ADDRESS and layers & DataLayer.POI:
133 orexpr.append(no_index(table.c.rank_address).between(1, 30))
134 elif layers & DataLayer.ADDRESS:
135 orexpr.append(no_index(table.c.rank_address).between(1, 29))
136 orexpr.append(sa.func.IsAddressPoint(table))
137 elif layers & DataLayer.POI:
138 orexpr.append(sa.and_(no_index(table.c.rank_address) == 30,
139 table.c.class_.not_in(('place', 'building'))))
141 if layers & DataLayer.MANMADE:
143 if not layers & DataLayer.RAILWAY:
144 exclude.append('railway')
145 if not layers & DataLayer.NATURAL:
146 exclude.extend(('natural', 'water', 'waterway'))
147 orexpr.append(sa.and_(table.c.class_.not_in(tuple(exclude)),
148 no_index(table.c.rank_address) == 0))
151 if layers & DataLayer.RAILWAY:
152 include.append('railway')
153 if layers & DataLayer.NATURAL:
154 include.extend(('natural', 'water', 'waterway'))
155 orexpr.append(sa.and_(table.c.class_.in_(tuple(include)),
156 no_index(table.c.rank_address) == 0))
161 return sa.or_(*orexpr)
164 def _interpolated_position(table: SaFromClause, nr: SaColumn) -> SaColumn:
165 pos = sa.cast(nr - table.c.startnumber, sa.Float) / (table.c.endnumber - table.c.startnumber)
167 (table.c.endnumber == table.c.startnumber, table.c.linegeo.ST_Centroid()),
168 else_=table.c.linegeo.ST_LineInterpolatePoint(pos)).label('centroid')
171 async def _get_placex_housenumbers(conn: SearchConnection,
172 place_ids: List[int],
173 details: SearchDetails) -> AsyncIterator[nres.SearchResult]:
175 sql = _select_placex(t).add_columns(t.c.importance)\
176 .where(t.c.place_id.in_(place_ids))
178 if details.geometry_output:
179 sql = _add_geometry_columns(sql, t.c.geometry, details)
181 for row in await conn.execute(sql):
182 result = nres.create_from_placex_row(row, nres.SearchResult)
184 result.bbox = Bbox.from_wkb(row.bbox)
188 def _int_list_to_subquery(inp: List[int]) -> 'sa.Subquery':
189 """ Create a subselect that returns the given list of integers
190 as rows in the column 'nr'.
192 vtab = sa.func.JsonArrayEach(sa.type_coerce(inp, sa.JSON))\
193 .table_valued(sa.column('value', type_=sa.JSON))
194 return sa.select(sa.cast(sa.cast(vtab.c.value, sa.Text), sa.Integer).label('nr')).subquery()
197 async def _get_osmline(conn: SearchConnection, place_ids: List[int],
199 details: SearchDetails) -> AsyncIterator[nres.SearchResult]:
202 values = _int_list_to_subquery(numerals)
203 sql = sa.select(t.c.place_id, t.c.osm_id,
204 t.c.parent_place_id, t.c.address,
205 values.c.nr.label('housenumber'),
206 _interpolated_position(t, values.c.nr),
207 t.c.postcode, t.c.country_code)\
208 .where(t.c.place_id.in_(place_ids))\
209 .join(values, values.c.nr.between(t.c.startnumber, t.c.endnumber))
211 if details.geometry_output:
213 sql = _add_geometry_columns(sa.select(sub), sub.c.centroid, details)
215 for row in await conn.execute(sql):
216 result = nres.create_from_osmline_row(row, nres.SearchResult)
221 async def _get_tiger(conn: SearchConnection, place_ids: List[int],
222 numerals: List[int], osm_id: int,
223 details: SearchDetails) -> AsyncIterator[nres.SearchResult]:
225 values = _int_list_to_subquery(numerals)
226 sql = sa.select(t.c.place_id, t.c.parent_place_id,
227 sa.literal('W').label('osm_type'),
228 sa.literal(osm_id).label('osm_id'),
229 values.c.nr.label('housenumber'),
230 _interpolated_position(t, values.c.nr),
232 .where(t.c.place_id.in_(place_ids))\
233 .join(values, values.c.nr.between(t.c.startnumber, t.c.endnumber))
235 if details.geometry_output:
237 sql = _add_geometry_columns(sa.select(sub), sub.c.centroid, details)
239 for row in await conn.execute(sql):
240 result = nres.create_from_tiger_row(row, nres.SearchResult)
245 class AbstractSearch(abc.ABC):
246 """ Encapuslation of a single lookup in the database.
250 def __init__(self, penalty: float) -> None:
251 self.penalty = penalty
254 async def lookup(self, conn: SearchConnection,
255 details: SearchDetails) -> nres.SearchResults:
256 """ Find results for the search in the database.
260 class NearSearch(AbstractSearch):
261 """ Category search of a place type near the result of another search.
263 def __init__(self, penalty: float, categories: WeightedCategories,
264 search: AbstractSearch) -> None:
265 super().__init__(penalty)
267 self.categories = categories
270 async def lookup(self, conn: SearchConnection,
271 details: SearchDetails) -> nres.SearchResults:
272 """ Find results for the search in the database.
274 results = nres.SearchResults()
275 base = await self.search.lookup(conn, details)
280 base.sort(key=lambda r: (r.accuracy, r.rank_search))
281 max_accuracy = base[0].accuracy + 0.5
282 if base[0].rank_address == 0:
285 elif base[0].rank_address < 26:
287 max_rank = min(25, base[0].rank_address + 4)
291 base = nres.SearchResults(r for r in base if r.source_table == nres.SourceTable.PLACEX
292 and r.accuracy <= max_accuracy
293 and r.bbox and r.bbox.area < 20
294 and r.rank_address >= min_rank
295 and r.rank_address <= max_rank)
298 baseids = [b.place_id for b in base[:5] if b.place_id]
300 for category, penalty in self.categories:
301 await self.lookup_category(results, conn, baseids, category, penalty, details)
302 if len(results) >= details.max_results:
308 async def lookup_category(self, results: nres.SearchResults,
309 conn: SearchConnection, ids: List[int],
310 category: Tuple[str, str], penalty: float,
311 details: SearchDetails) -> None:
312 """ Find places of the given category near the list of
313 place ids and add the results to 'results'.
315 table = await conn.get_class_table(*category)
317 tgeom = conn.t.placex.alias('pgeom')
320 # No classtype table available, do a simplified lookup in placex.
321 table = conn.t.placex
322 sql = sa.select(table.c.place_id,
323 sa.func.min(tgeom.c.centroid.ST_Distance(table.c.centroid))
325 .join(tgeom, table.c.geometry.intersects(tgeom.c.centroid.ST_Expand(0.01)))\
326 .where(table.c.class_ == category[0])\
327 .where(table.c.type == category[1])
329 # Use classtype table. We can afford to use a larger
330 # radius for the lookup.
331 sql = sa.select(table.c.place_id,
332 sa.func.min(tgeom.c.centroid.ST_Distance(table.c.centroid))
335 table.c.centroid.ST_CoveredBy(
336 sa.case((sa.and_(tgeom.c.rank_address > 9,
337 tgeom.c.geometry.is_area()),
339 else_ = tgeom.c.centroid.ST_Expand(0.05))))
341 inner = sql.where(tgeom.c.place_id.in_(ids))\
342 .group_by(table.c.place_id).subquery()
345 sql = _select_placex(t).add_columns((-inner.c.dist).label('importance'))\
346 .join(inner, inner.c.place_id == t.c.place_id)\
347 .order_by(inner.c.dist)
349 sql = sql.where(no_index(t.c.rank_address).between(MIN_RANK_PARAM, MAX_RANK_PARAM))
350 if details.countries:
351 sql = sql.where(t.c.country_code.in_(COUNTRIES_PARAM))
353 sql = sql.where(_exclude_places(t))
354 if details.layers is not None:
355 sql = sql.where(_filter_by_layer(t, details.layers))
357 sql = sql.limit(LIMIT_PARAM)
358 for row in await conn.execute(sql, _details_to_bind_params(details)):
359 result = nres.create_from_placex_row(row, nres.SearchResult)
361 result.accuracy = self.penalty + penalty
362 result.bbox = Bbox.from_wkb(row.bbox)
363 results.append(result)
367 class PoiSearch(AbstractSearch):
368 """ Category search in a geographic area.
370 def __init__(self, sdata: SearchData) -> None:
371 super().__init__(sdata.penalty)
372 self.qualifiers = sdata.qualifiers
373 self.countries = sdata.countries
376 async def lookup(self, conn: SearchConnection,
377 details: SearchDetails) -> nres.SearchResults:
378 """ Find results for the search in the database.
380 bind_params = _details_to_bind_params(details)
383 rows: List[SaRow] = []
385 if details.near and details.near_radius is not None and details.near_radius < 0.2:
386 # simply search in placex table
387 def _base_query() -> SaSelect:
388 return _select_placex(t) \
389 .add_columns((-t.c.centroid.ST_Distance(NEAR_PARAM))
390 .label('importance'))\
391 .where(t.c.linked_place_id == None) \
392 .where(t.c.geometry.within_distance(NEAR_PARAM, NEAR_RADIUS_PARAM)) \
393 .order_by(t.c.centroid.ST_Distance(NEAR_PARAM)) \
396 classtype = self.qualifiers.values
397 if len(classtype) == 1:
398 cclass, ctype = classtype[0]
399 sql: SaLambdaSelect = sa.lambda_stmt(lambda: _base_query()
400 .where(t.c.class_ == cclass)
401 .where(t.c.type == ctype))
403 sql = _base_query().where(sa.or_(*(sa.and_(t.c.class_ == cls, t.c.type == typ)
404 for cls, typ in classtype)))
407 sql = sql.where(t.c.country_code.in_(self.countries.values))
409 if details.viewbox is not None and details.bounded_viewbox:
410 sql = sql.where(t.c.geometry.intersects(VIEWBOX_PARAM))
412 rows.extend(await conn.execute(sql, bind_params))
414 # use the class type tables
415 for category in self.qualifiers.values:
416 table = await conn.get_class_table(*category)
417 if table is not None:
418 sql = _select_placex(t)\
419 .add_columns(t.c.importance)\
420 .join(table, t.c.place_id == table.c.place_id)\
421 .where(t.c.class_ == category[0])\
422 .where(t.c.type == category[1])
424 if details.viewbox is not None and details.bounded_viewbox:
425 sql = sql.where(table.c.centroid.intersects(VIEWBOX_PARAM))
427 if details.near and details.near_radius is not None:
428 sql = sql.order_by(table.c.centroid.ST_Distance(NEAR_PARAM))\
429 .where(table.c.centroid.within_distance(NEAR_PARAM,
433 sql = sql.where(t.c.country_code.in_(self.countries.values))
435 sql = sql.limit(LIMIT_PARAM)
436 rows.extend(await conn.execute(sql, bind_params))
438 results = nres.SearchResults()
440 result = nres.create_from_placex_row(row, nres.SearchResult)
442 result.accuracy = self.penalty + self.qualifiers.get_penalty((row.class_, row.type))
443 result.bbox = Bbox.from_wkb(row.bbox)
444 results.append(result)
449 class CountrySearch(AbstractSearch):
450 """ Search for a country name or country code.
454 def __init__(self, sdata: SearchData) -> None:
455 super().__init__(sdata.penalty)
456 self.countries = sdata.countries
459 async def lookup(self, conn: SearchConnection,
460 details: SearchDetails) -> nres.SearchResults:
461 """ Find results for the search in the database.
465 ccodes = self.countries.values
466 sql = _select_placex(t)\
467 .add_columns(t.c.importance)\
468 .where(t.c.country_code.in_(ccodes))\
469 .where(t.c.rank_address == 4)
471 if details.geometry_output:
472 sql = _add_geometry_columns(sql, t.c.geometry, details)
475 sql = sql.where(_exclude_places(t))
477 sql = filter_by_area(sql, t, details)
479 results = nres.SearchResults()
480 for row in await conn.execute(sql, _details_to_bind_params(details)):
481 result = nres.create_from_placex_row(row, nres.SearchResult)
483 result.accuracy = self.penalty + self.countries.get_penalty(row.country_code, 5.0)
484 result.bbox = Bbox.from_wkb(row.bbox)
485 results.append(result)
487 return results or await self.lookup_in_country_table(conn, details)
490 async def lookup_in_country_table(self, conn: SearchConnection,
491 details: SearchDetails) -> nres.SearchResults:
492 """ Look up the country in the fallback country tables.
494 # Avoid the fallback search when this is a more search. Country results
495 # usually are in the first batch of results and it is not possible
496 # to exclude these fallbacks.
498 return nres.SearchResults()
500 t = conn.t.country_name
501 tgrid = conn.t.country_grid
503 sql = sa.select(tgrid.c.country_code,
504 tgrid.c.geometry.ST_Centroid().ST_Collect().ST_Centroid()
506 tgrid.c.geometry.ST_Collect().ST_Expand(0).label('bbox'))\
507 .where(tgrid.c.country_code.in_(self.countries.values))\
508 .group_by(tgrid.c.country_code)
510 sql = filter_by_area(sql, tgrid, details, avoid_index=True)
512 sub = sql.subquery('grid')
514 sql = sa.select(t.c.country_code,
515 t.c.name.merge(t.c.derived_name).label('name'),
516 sub.c.centroid, sub.c.bbox)\
517 .join(sub, t.c.country_code == sub.c.country_code)
519 if details.geometry_output:
520 sql = _add_geometry_columns(sql, sub.c.centroid, details)
522 results = nres.SearchResults()
523 for row in await conn.execute(sql, _details_to_bind_params(details)):
524 result = nres.create_from_country_row(row, nres.SearchResult)
526 result.bbox = Bbox.from_wkb(row.bbox)
527 result.accuracy = self.penalty + self.countries.get_penalty(row.country_code, 5.0)
528 results.append(result)
534 class PostcodeSearch(AbstractSearch):
535 """ Search for a postcode.
537 def __init__(self, extra_penalty: float, sdata: SearchData) -> None:
538 super().__init__(sdata.penalty + extra_penalty)
539 self.countries = sdata.countries
540 self.postcodes = sdata.postcodes
541 self.lookups = sdata.lookups
542 self.rankings = sdata.rankings
545 async def lookup(self, conn: SearchConnection,
546 details: SearchDetails) -> nres.SearchResults:
547 """ Find results for the search in the database.
550 pcs = self.postcodes.values
552 sql = sa.select(t.c.place_id, t.c.parent_place_id,
553 t.c.rank_search, t.c.rank_address,
554 t.c.postcode, t.c.country_code,
555 t.c.geometry.label('centroid'))\
556 .where(t.c.postcode.in_(pcs))
558 if details.geometry_output:
559 sql = _add_geometry_columns(sql, t.c.geometry, details)
561 penalty: SaExpression = sa.literal(self.penalty)
563 if details.viewbox is not None and not details.bounded_viewbox:
564 penalty += sa.case((t.c.geometry.intersects(VIEWBOX_PARAM), 0.0),
565 (t.c.geometry.intersects(VIEWBOX2_PARAM), 0.5),
568 if details.near is not None:
569 sql = sql.order_by(t.c.geometry.ST_Distance(NEAR_PARAM))
571 sql = filter_by_area(sql, t, details)
574 sql = sql.where(t.c.country_code.in_(self.countries.values))
577 sql = sql.where(_exclude_places(t))
580 assert len(self.lookups) == 1
581 tsearch = conn.t.search_name
582 sql = sql.where(tsearch.c.place_id == t.c.parent_place_id)\
583 .where((tsearch.c.name_vector + tsearch.c.nameaddress_vector)
584 .contains(sa.type_coerce(self.lookups[0].tokens,
587 for ranking in self.rankings:
588 penalty += ranking.sql_penalty(conn.t.search_name)
589 penalty += sa.case(*((t.c.postcode == v, p) for v, p in self.postcodes),
593 sql = sql.add_columns(penalty.label('accuracy'))
594 sql = sql.order_by('accuracy').limit(LIMIT_PARAM)
596 results = nres.SearchResults()
597 for row in await conn.execute(sql, _details_to_bind_params(details)):
598 result = nres.create_from_postcode_row(row, nres.SearchResult)
600 result.accuracy = row.accuracy
601 results.append(result)
607 class PlaceSearch(AbstractSearch):
608 """ Generic search for an address or named place.
612 def __init__(self, extra_penalty: float, sdata: SearchData, expected_count: int) -> None:
613 super().__init__(sdata.penalty + extra_penalty)
614 self.countries = sdata.countries
615 self.postcodes = sdata.postcodes
616 self.housenumbers = sdata.housenumbers
617 self.qualifiers = sdata.qualifiers
618 self.lookups = sdata.lookups
619 self.rankings = sdata.rankings
620 self.expected_count = expected_count
623 async def lookup(self, conn: SearchConnection,
624 details: SearchDetails) -> nres.SearchResults:
625 """ Find results for the search in the database.
628 tsearch = conn.t.search_name
630 sql: SaLambdaSelect = sa.lambda_stmt(lambda:
631 _select_placex(t).where(t.c.place_id == tsearch.c.place_id))
634 if details.geometry_output:
635 sql = _add_geometry_columns(sql, t.c.geometry, details)
637 penalty: SaExpression = sa.literal(self.penalty)
638 for ranking in self.rankings:
639 penalty += ranking.sql_penalty(tsearch)
641 for lookup in self.lookups:
642 sql = sql.where(lookup.sql_condition(tsearch))
645 sql = sql.where(tsearch.c.country_code.in_(self.countries.values))
648 # if a postcode is given, don't search for state or country level objects
649 sql = sql.where(tsearch.c.address_rank > 9)
650 tpc = conn.t.postcode
651 pcs = self.postcodes.values
652 if self.expected_count > 5000:
653 # Many results expected. Restrict by postcode.
654 sql = sql.where(sa.select(tpc.c.postcode)
655 .where(tpc.c.postcode.in_(pcs))
656 .where(tsearch.c.centroid.within_distance(tpc.c.geometry, 0.12))
659 # Less results, only have a preference for close postcodes
660 pc_near = sa.select(sa.func.min(tpc.c.geometry.ST_Distance(tsearch.c.centroid)))\
661 .where(tpc.c.postcode.in_(pcs))\
663 penalty += sa.case((t.c.postcode.in_(pcs), 0.0),
664 else_=sa.func.coalesce(pc_near, cast(SaColumn, 2.0)))
666 if details.viewbox is not None:
667 if details.bounded_viewbox:
668 sql = sql.where(tsearch.c.centroid
669 .intersects(VIEWBOX_PARAM,
670 use_index=details.viewbox.area < 0.2))
671 elif not self.postcodes and not self.housenumbers and self.expected_count >= 10000:
672 sql = sql.where(tsearch.c.centroid
673 .intersects(VIEWBOX2_PARAM,
674 use_index=details.viewbox.area < 0.5))
676 penalty += sa.case((t.c.geometry.intersects(VIEWBOX_PARAM, use_index=False), 0.0),
677 (t.c.geometry.intersects(VIEWBOX2_PARAM, use_index=False), 0.5),
680 if details.near is not None:
681 if details.near_radius is not None:
682 if details.near_radius < 0.1:
683 sql = sql.where(tsearch.c.centroid.within_distance(NEAR_PARAM,
686 sql = sql.where(tsearch.c.centroid
687 .ST_Distance(NEAR_PARAM) < NEAR_RADIUS_PARAM)
688 sql = sql.add_columns((-tsearch.c.centroid.ST_Distance(NEAR_PARAM))
689 .label('importance'))
690 sql = sql.order_by(sa.desc(sa.text('importance')))
692 if self.expected_count < 10000\
693 or (details.viewbox is not None and details.viewbox.area < 0.5):
695 penalty - sa.case((tsearch.c.importance > 0, tsearch.c.importance),
696 else_=0.75001-(sa.cast(tsearch.c.search_rank, sa.Float())/40)))
697 sql = sql.add_columns(t.c.importance)
700 sql = sql.add_columns(penalty.label('accuracy'))
702 if self.expected_count < 10000:
703 sql = sql.order_by(sa.text('accuracy'))
705 if self.housenumbers:
706 hnr_list = '|'.join(self.housenumbers.values)
707 sql = sql.where(tsearch.c.address_rank.between(16, 30))\
708 .where(sa.or_(tsearch.c.address_rank < 30,
709 sa.func.RegexpWord(hnr_list, t.c.housenumber)))
711 # Cross check for housenumbers, need to do that on a rather large
712 # set. Worst case there are 40.000 main streets in OSM.
713 inner = sql.limit(10000).subquery()
715 # Housenumbers from placex
716 thnr = conn.t.placex.alias('hnr')
717 pid_list = sa.func.ArrayAgg(thnr.c.place_id)
718 place_sql = sa.select(pid_list)\
719 .where(thnr.c.parent_place_id == inner.c.place_id)\
720 .where(sa.func.RegexpWord(hnr_list, thnr.c.housenumber))\
721 .where(thnr.c.linked_place_id == None)\
722 .where(thnr.c.indexed_status == 0)
725 place_sql = place_sql.where(thnr.c.place_id.not_in(sa.bindparam('excluded')))
727 place_sql = place_sql.where(self.qualifiers.sql_restrict(thnr))
729 numerals = [int(n) for n in self.housenumbers.values
730 if n.isdigit() and len(n) < 8]
731 interpol_sql: SaColumn
734 (not self.qualifiers or ('place', 'house') in self.qualifiers.values):
735 # Housenumbers from interpolations
736 interpol_sql = _make_interpolation_subquery(conn.t.osmline, inner,
738 # Housenumbers from Tiger
739 tiger_sql = sa.case((inner.c.country_code == 'us',
740 _make_interpolation_subquery(conn.t.tiger, inner,
744 interpol_sql = sa.null()
745 tiger_sql = sa.null()
747 unsort = sa.select(inner, place_sql.scalar_subquery().label('placex_hnr'),
748 interpol_sql.label('interpol_hnr'),
749 tiger_sql.label('tiger_hnr')).subquery('unsort')
750 sql = sa.select(unsort)\
751 .order_by(sa.case((unsort.c.placex_hnr != None, 1),
752 (unsort.c.interpol_hnr != None, 2),
753 (unsort.c.tiger_hnr != None, 3),
757 sql = sql.where(t.c.linked_place_id == None)\
758 .where(t.c.indexed_status == 0)
760 sql = sql.where(self.qualifiers.sql_restrict(t))
762 sql = sql.where(_exclude_places(tsearch))
763 if details.min_rank > 0:
764 sql = sql.where(sa.or_(tsearch.c.address_rank >= MIN_RANK_PARAM,
765 tsearch.c.search_rank >= MIN_RANK_PARAM))
766 if details.max_rank < 30:
767 sql = sql.where(sa.or_(tsearch.c.address_rank <= MAX_RANK_PARAM,
768 tsearch.c.search_rank <= MAX_RANK_PARAM))
769 if details.layers is not None:
770 sql = sql.where(_filter_by_layer(t, details.layers))
772 sql = sql.limit(LIMIT_PARAM)
774 results = nres.SearchResults()
775 for row in await conn.execute(sql, _details_to_bind_params(details)):
776 result = nres.create_from_placex_row(row, nres.SearchResult)
778 result.bbox = Bbox.from_wkb(row.bbox)
779 result.accuracy = row.accuracy
780 if self.housenumbers and row.rank_address < 30:
782 subs = _get_placex_housenumbers(conn, row.placex_hnr, details)
783 elif row.interpol_hnr:
784 subs = _get_osmline(conn, row.interpol_hnr, numerals, details)
786 subs = _get_tiger(conn, row.tiger_hnr, numerals, row.osm_id, details)
791 async for sub in subs:
792 assert sub.housenumber
793 sub.accuracy = result.accuracy
794 if not any(nr in self.housenumbers.values
795 for nr in sub.housenumber.split(';')):
799 # Only add the street as a result, if it meets all other
801 if (not details.excluded or result.place_id not in details.excluded)\
802 and (not self.qualifiers or result.category in self.qualifiers.values)\
803 and result.rank_address >= details.min_rank:
804 result.accuracy += 1.0 # penalty for missing housenumber
805 results.append(result)
807 results.append(result)