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)
488 results = await self.lookup_in_country_table(conn, details)
491 details.min_rank = min(5, details.max_rank)
492 details.max_rank = min(25, details.max_rank)
497 async def lookup_in_country_table(self, conn: SearchConnection,
498 details: SearchDetails) -> nres.SearchResults:
499 """ Look up the country in the fallback country tables.
501 # Avoid the fallback search when this is a more search. Country results
502 # usually are in the first batch of results and it is not possible
503 # to exclude these fallbacks.
505 return nres.SearchResults()
507 t = conn.t.country_name
508 tgrid = conn.t.country_grid
510 sql = sa.select(tgrid.c.country_code,
511 tgrid.c.geometry.ST_Centroid().ST_Collect().ST_Centroid()
513 tgrid.c.geometry.ST_Collect().ST_Expand(0).label('bbox'))\
514 .where(tgrid.c.country_code.in_(self.countries.values))\
515 .group_by(tgrid.c.country_code)
517 sql = filter_by_area(sql, tgrid, details, avoid_index=True)
519 sub = sql.subquery('grid')
521 sql = sa.select(t.c.country_code,
522 t.c.name.merge(t.c.derived_name).label('name'),
523 sub.c.centroid, sub.c.bbox)\
524 .join(sub, t.c.country_code == sub.c.country_code)
526 if details.geometry_output:
527 sql = _add_geometry_columns(sql, sub.c.centroid, details)
529 results = nres.SearchResults()
530 for row in await conn.execute(sql, _details_to_bind_params(details)):
531 result = nres.create_from_country_row(row, nres.SearchResult)
533 result.bbox = Bbox.from_wkb(row.bbox)
534 result.accuracy = self.penalty + self.countries.get_penalty(row.country_code, 5.0)
535 results.append(result)
541 class PostcodeSearch(AbstractSearch):
542 """ Search for a postcode.
544 def __init__(self, extra_penalty: float, sdata: SearchData) -> None:
545 super().__init__(sdata.penalty + extra_penalty)
546 self.countries = sdata.countries
547 self.postcodes = sdata.postcodes
548 self.lookups = sdata.lookups
549 self.rankings = sdata.rankings
552 async def lookup(self, conn: SearchConnection,
553 details: SearchDetails) -> nres.SearchResults:
554 """ Find results for the search in the database.
557 pcs = self.postcodes.values
559 sql = sa.select(t.c.place_id, t.c.parent_place_id,
560 t.c.rank_search, t.c.rank_address,
561 t.c.postcode, t.c.country_code,
562 t.c.geometry.label('centroid'))\
563 .where(t.c.postcode.in_(pcs))
565 if details.geometry_output:
566 sql = _add_geometry_columns(sql, t.c.geometry, details)
568 penalty: SaExpression = sa.literal(self.penalty)
570 if details.viewbox is not None and not details.bounded_viewbox:
571 penalty += sa.case((t.c.geometry.intersects(VIEWBOX_PARAM), 0.0),
572 (t.c.geometry.intersects(VIEWBOX2_PARAM), 0.5),
575 if details.near is not None:
576 sql = sql.order_by(t.c.geometry.ST_Distance(NEAR_PARAM))
578 sql = filter_by_area(sql, t, details)
581 sql = sql.where(t.c.country_code.in_(self.countries.values))
584 sql = sql.where(_exclude_places(t))
587 assert len(self.lookups) == 1
588 tsearch = conn.t.search_name
589 sql = sql.where(tsearch.c.place_id == t.c.parent_place_id)\
590 .where((tsearch.c.name_vector + tsearch.c.nameaddress_vector)
591 .contains(sa.type_coerce(self.lookups[0].tokens,
594 for ranking in self.rankings:
595 penalty += ranking.sql_penalty(conn.t.search_name)
596 penalty += sa.case(*((t.c.postcode == v, p) for v, p in self.postcodes),
600 sql = sql.add_columns(penalty.label('accuracy'))
601 sql = sql.order_by('accuracy').limit(LIMIT_PARAM)
603 results = nres.SearchResults()
604 for row in await conn.execute(sql, _details_to_bind_params(details)):
605 result = nres.create_from_postcode_row(row, nres.SearchResult)
607 result.accuracy = row.accuracy
608 results.append(result)
614 class PlaceSearch(AbstractSearch):
615 """ Generic search for an address or named place.
619 def __init__(self, extra_penalty: float, sdata: SearchData, expected_count: int) -> None:
620 super().__init__(sdata.penalty + extra_penalty)
621 self.countries = sdata.countries
622 self.postcodes = sdata.postcodes
623 self.housenumbers = sdata.housenumbers
624 self.qualifiers = sdata.qualifiers
625 self.lookups = sdata.lookups
626 self.rankings = sdata.rankings
627 self.expected_count = expected_count
630 async def lookup(self, conn: SearchConnection,
631 details: SearchDetails) -> nres.SearchResults:
632 """ Find results for the search in the database.
635 tsearch = conn.t.search_name
637 sql: SaLambdaSelect = sa.lambda_stmt(lambda:
638 _select_placex(t).where(t.c.place_id == tsearch.c.place_id))
641 if details.geometry_output:
642 sql = _add_geometry_columns(sql, t.c.geometry, details)
644 penalty: SaExpression = sa.literal(self.penalty)
645 for ranking in self.rankings:
646 penalty += ranking.sql_penalty(tsearch)
648 for lookup in self.lookups:
649 sql = sql.where(lookup.sql_condition(tsearch))
652 sql = sql.where(tsearch.c.country_code.in_(self.countries.values))
655 # if a postcode is given, don't search for state or country level objects
656 sql = sql.where(tsearch.c.address_rank > 9)
657 tpc = conn.t.postcode
658 pcs = self.postcodes.values
659 if self.expected_count > 5000:
660 # Many results expected. Restrict by postcode.
661 sql = sql.where(sa.select(tpc.c.postcode)
662 .where(tpc.c.postcode.in_(pcs))
663 .where(tsearch.c.centroid.within_distance(tpc.c.geometry, 0.12))
666 # Less results, only have a preference for close postcodes
667 pc_near = sa.select(sa.func.min(tpc.c.geometry.ST_Distance(tsearch.c.centroid)))\
668 .where(tpc.c.postcode.in_(pcs))\
670 penalty += sa.case((t.c.postcode.in_(pcs), 0.0),
671 else_=sa.func.coalesce(pc_near, cast(SaColumn, 2.0)))
673 if details.viewbox is not None:
674 if details.bounded_viewbox:
675 sql = sql.where(tsearch.c.centroid
676 .intersects(VIEWBOX_PARAM,
677 use_index=details.viewbox.area < 0.2))
678 elif not self.postcodes and not self.housenumbers and self.expected_count >= 10000:
679 sql = sql.where(tsearch.c.centroid
680 .intersects(VIEWBOX2_PARAM,
681 use_index=details.viewbox.area < 0.5))
683 penalty += sa.case((t.c.geometry.intersects(VIEWBOX_PARAM, use_index=False), 0.0),
684 (t.c.geometry.intersects(VIEWBOX2_PARAM, use_index=False), 0.5),
687 if details.near is not None:
688 if details.near_radius is not None:
689 if details.near_radius < 0.1:
690 sql = sql.where(tsearch.c.centroid.within_distance(NEAR_PARAM,
693 sql = sql.where(tsearch.c.centroid
694 .ST_Distance(NEAR_PARAM) < NEAR_RADIUS_PARAM)
695 sql = sql.add_columns((-tsearch.c.centroid.ST_Distance(NEAR_PARAM))
696 .label('importance'))
697 sql = sql.order_by(sa.desc(sa.text('importance')))
699 if self.expected_count < 10000\
700 or (details.viewbox is not None and details.viewbox.area < 0.5):
702 penalty - sa.case((tsearch.c.importance > 0, tsearch.c.importance),
703 else_=0.40001-(sa.cast(tsearch.c.search_rank, sa.Float())/75)))
704 sql = sql.add_columns(t.c.importance)
707 sql = sql.add_columns(penalty.label('accuracy'))
709 if self.expected_count < 10000:
710 sql = sql.order_by(sa.text('accuracy'))
712 if self.housenumbers:
713 hnr_list = '|'.join(self.housenumbers.values)
714 sql = sql.where(tsearch.c.address_rank.between(16, 30))\
715 .where(sa.or_(tsearch.c.address_rank < 30,
716 sa.func.RegexpWord(hnr_list, t.c.housenumber)))
718 # Cross check for housenumbers, need to do that on a rather large
719 # set. Worst case there are 40.000 main streets in OSM.
720 inner = sql.limit(10000).subquery()
722 # Housenumbers from placex
723 thnr = conn.t.placex.alias('hnr')
724 pid_list = sa.func.ArrayAgg(thnr.c.place_id)
725 place_sql = sa.select(pid_list)\
726 .where(thnr.c.parent_place_id == inner.c.place_id)\
727 .where(sa.func.RegexpWord(hnr_list, thnr.c.housenumber))\
728 .where(thnr.c.linked_place_id == None)\
729 .where(thnr.c.indexed_status == 0)
732 place_sql = place_sql.where(thnr.c.place_id.not_in(sa.bindparam('excluded')))
734 place_sql = place_sql.where(self.qualifiers.sql_restrict(thnr))
736 numerals = [int(n) for n in self.housenumbers.values
737 if n.isdigit() and len(n) < 8]
738 interpol_sql: SaColumn
741 (not self.qualifiers or ('place', 'house') in self.qualifiers.values):
742 # Housenumbers from interpolations
743 interpol_sql = _make_interpolation_subquery(conn.t.osmline, inner,
745 # Housenumbers from Tiger
746 tiger_sql = sa.case((inner.c.country_code == 'us',
747 _make_interpolation_subquery(conn.t.tiger, inner,
751 interpol_sql = sa.null()
752 tiger_sql = sa.null()
754 unsort = sa.select(inner, place_sql.scalar_subquery().label('placex_hnr'),
755 interpol_sql.label('interpol_hnr'),
756 tiger_sql.label('tiger_hnr')).subquery('unsort')
757 sql = sa.select(unsort)\
758 .order_by(sa.case((unsort.c.placex_hnr != None, 1),
759 (unsort.c.interpol_hnr != None, 2),
760 (unsort.c.tiger_hnr != None, 3),
764 sql = sql.where(t.c.linked_place_id == None)\
765 .where(t.c.indexed_status == 0)
767 sql = sql.where(self.qualifiers.sql_restrict(t))
769 sql = sql.where(_exclude_places(tsearch))
770 if details.min_rank > 0:
771 sql = sql.where(sa.or_(tsearch.c.address_rank >= MIN_RANK_PARAM,
772 tsearch.c.search_rank >= MIN_RANK_PARAM))
773 if details.max_rank < 30:
774 sql = sql.where(sa.or_(tsearch.c.address_rank <= MAX_RANK_PARAM,
775 tsearch.c.search_rank <= MAX_RANK_PARAM))
776 if details.layers is not None:
777 sql = sql.where(_filter_by_layer(t, details.layers))
779 sql = sql.limit(LIMIT_PARAM)
781 results = nres.SearchResults()
782 for row in await conn.execute(sql, _details_to_bind_params(details)):
783 result = nres.create_from_placex_row(row, nres.SearchResult)
785 result.bbox = Bbox.from_wkb(row.bbox)
786 result.accuracy = row.accuracy
787 if self.housenumbers and row.rank_address < 30:
789 subs = _get_placex_housenumbers(conn, row.placex_hnr, details)
790 elif row.interpol_hnr:
791 subs = _get_osmline(conn, row.interpol_hnr, numerals, details)
793 subs = _get_tiger(conn, row.tiger_hnr, numerals, row.osm_id, details)
798 async for sub in subs:
799 assert sub.housenumber
800 sub.accuracy = result.accuracy
801 if not any(nr in self.housenumbers.values
802 for nr in sub.housenumber.split(';')):
806 # Only add the street as a result, if it meets all other
808 if (not details.excluded or result.place_id not in details.excluded)\
809 and (not self.qualifiers or result.category in self.qualifiers.values)\
810 and result.rank_address >= details.min_rank:
811 result.accuracy += 1.0 # penalty for missing housenumber
812 results.append(result)
814 results.append(result)