Skip to content

Commit 4cfc179

Browse files
authored
Merge pull request #3899 from lonvia/improve-reverse-performance
Streamline reverse lookup slightly
2 parents 6b12501 + 3bb5d00 commit 4cfc179

File tree

2 files changed

+99
-69
lines changed

2 files changed

+99
-69
lines changed

src/nominatim_api/reverse.py

Lines changed: 94 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -157,26 +157,27 @@ def _filter_by_layer(self, table: SaFromClause) -> SaColumn:
157157
include.extend(('natural', 'water', 'waterway'))
158158
return table.c.class_.in_(tuple(include))
159159

160-
async def _find_closest_street_or_poi(self, distance: float) -> Optional[SaRow]:
161-
""" Look up the closest rank 26+ place in the database, which
162-
is closer than the given distance.
160+
async def _find_closest_street_or_pois(self, distance: float,
161+
fuzziness: float) -> list[SaRow]:
162+
""" Look up the closest rank 26+ place in the database.
163+
The function finds the object that is closest to the reverse
164+
search point as well as all objects within 'fuzziness' distance
165+
to that best result.
163166
"""
164167
t = self.conn.t.placex
165168

166169
# PostgreSQL must not get the distance as a parameter because
167170
# there is a danger it won't be able to properly estimate index use
168171
# when used with prepared statements
169-
diststr = sa.text(f"{distance}")
172+
diststr = sa.text(f"{distance + fuzziness}")
170173

171174
sql: SaLambdaSelect = sa.lambda_stmt(
172175
lambda: _select_from_placex(t)
173176
.where(t.c.geometry.within_distance(WKT_PARAM, diststr))
174177
.where(t.c.indexed_status == 0)
175178
.where(t.c.linked_place_id == None)
176179
.where(sa.or_(sa.not_(t.c.geometry.is_area()),
177-
t.c.centroid.ST_Distance(WKT_PARAM) < diststr))
178-
.order_by('distance')
179-
.limit(2))
180+
t.c.centroid.ST_Distance(WKT_PARAM) < diststr)))
180181

181182
if self.has_geometries():
182183
sql = self._add_geometry_columns(sql, t.c.geometry)
@@ -198,24 +199,39 @@ async def _find_closest_street_or_poi(self, distance: float) -> Optional[SaRow]:
198199
self._filter_by_layer(t)))
199200

200201
if not restrict:
201-
return None
202-
203-
sql = sql.where(sa.or_(*restrict))
204-
205-
# If the closest object is inside an area, then check if there is a
206-
# POI node nearby and return that.
207-
prev_row = None
208-
for row in await self.conn.execute(sql, self.bind_params):
209-
if prev_row is None:
210-
if row.rank_search <= 27 or row.osm_type == 'N' or row.distance > 0:
211-
return row
212-
prev_row = row
213-
else:
214-
if row.rank_search > 27 and row.osm_type == 'N'\
215-
and row.distance < 0.0001:
216-
return row
217-
218-
return prev_row
202+
return []
203+
204+
inner = sql.where(sa.or_(*restrict)) \
205+
.add_columns(t.c.geometry.label('_geometry')) \
206+
.subquery()
207+
208+
# Use a window function to get the closest results to the best result.
209+
windowed = sa.select(inner,
210+
sa.func.first_value(inner.c.distance)
211+
.over(order_by=inner.c.distance)
212+
.label('_min_distance'),
213+
sa.func.first_value(inner.c._geometry.ST_ClosestPoint(WKT_PARAM))
214+
.over(order_by=inner.c.distance)
215+
.label('_closest_point'),
216+
sa.func.first_value(sa.case((sa.or_(inner.c.rank_search <= 27,
217+
inner.c.osm_type == 'N'), None),
218+
else_=inner.c._geometry))
219+
.over(order_by=inner.c.distance)
220+
.label('_best_geometry')) \
221+
.subquery()
222+
223+
outer = sa.select(*(c for c in windowed.c if not c.key.startswith('_')),
224+
windowed.c.centroid.ST_Distance(windowed.c._closest_point)
225+
.label('best_distance'),
226+
sa.case((sa.or_(windowed.c._best_geometry == None,
227+
windowed.c.rank_search <= 27,
228+
windowed.c.osm_type != 'N'), False),
229+
else_=windowed.c.centroid.ST_CoveredBy(windowed.c._best_geometry))
230+
.label('best_inside')) \
231+
.where(windowed.c.distance < windowed.c._min_distance + fuzziness) \
232+
.order_by(windowed.c.distance)
233+
234+
return list(await self.conn.execute(outer, self.bind_params))
219235

220236
async def _find_housenumber_for_street(self, parent_place_id: int) -> Optional[SaRow]:
221237
t = self.conn.t.placex
@@ -301,55 +317,69 @@ async def lookup_street_poi(self) -> Tuple[Optional[SaRow], RowFunc]:
301317
""" Find a street or POI/address for the given WKT point.
302318
"""
303319
log().section('Reverse lookup on street/address level')
320+
row_func: RowFunc = nres.create_from_placex_row
304321
distance = 0.006
305-
parent_place_id = None
306322

307-
row = await self._find_closest_street_or_poi(distance)
308-
row_func: RowFunc = nres.create_from_placex_row
309-
log().var_dump('Result (street/building)', row)
310-
311-
# If the closest result was a street, but an address was requested,
312-
# check for a housenumber nearby which is part of the street.
313-
if row is not None:
314-
if self.max_rank > 27 \
315-
and self.layer_enabled(DataLayer.ADDRESS) \
316-
and row.rank_address <= 27:
317-
distance = 0.001
318-
parent_place_id = row.place_id
319-
log().comment('Find housenumber for street')
320-
addr_row = await self._find_housenumber_for_street(parent_place_id)
321-
log().var_dump('Result (street housenumber)', addr_row)
322-
323-
if addr_row is not None:
324-
row = addr_row
325-
row_func = nres.create_from_placex_row
326-
distance = addr_row.distance
327-
elif row.country_code == 'us' and parent_place_id is not None:
328-
log().comment('Find TIGER housenumber for street')
329-
addr_row = await self._find_tiger_number_for_street(parent_place_id)
330-
log().var_dump('Result (street Tiger housenumber)', addr_row)
331-
332-
if addr_row is not None:
333-
row_func = cast(RowFunc,
334-
functools.partial(nres.create_from_tiger_row,
335-
osm_type=row.osm_type,
336-
osm_id=row.osm_id))
337-
row = addr_row
338-
else:
323+
result = None
324+
hnr_distance = None
325+
parent_street = None
326+
for row in await self._find_closest_street_or_pois(distance, 0.001):
327+
if result is None:
328+
log().var_dump('Closest result', row)
329+
result = row
330+
if self.max_rank > 27 \
331+
and self.layer_enabled(DataLayer.ADDRESS) \
332+
and result.rank_address <= 27:
333+
parent_street = result.place_id
334+
distance = 0.001
335+
else:
336+
distance = row.distance
337+
# If the closest result was a street but an address was requested,
338+
# see if we can refine the result with a housenumber closeby.
339+
elif parent_street is not None \
340+
and row.rank_address > 27 \
341+
and row.best_distance < 0.001 \
342+
and (hnr_distance is None or hnr_distance > row.best_distance) \
343+
and row.parent_place_id == parent_street:
344+
log().var_dump('Housenumber to closest result', row)
345+
result = row
346+
hnr_distance = row.best_distance
339347
distance = row.distance
348+
# If the closest object is inside an area, then check if there is
349+
# a POI nearby and return that with preference.
350+
elif result.osm_type != 'N' and result.rank_search > 27 \
351+
and result.distance == 0 \
352+
and row.best_inside:
353+
log().var_dump('POI near closest result area', row)
354+
result = row
355+
break # it can't get better than that, everything else is farther away
356+
357+
# For the US also check the TIGER data, when no housenumber/POI was found.
358+
if result is not None and parent_street is not None and hnr_distance is None \
359+
and result.country_code == 'us':
360+
log().comment('Find TIGER housenumber for street')
361+
addr_row = await self._find_tiger_number_for_street(parent_street)
362+
log().var_dump('Result (street Tiger housenumber)', addr_row)
363+
364+
if addr_row is not None:
365+
row_func = cast(RowFunc,
366+
functools.partial(nres.create_from_tiger_row,
367+
osm_type=row.osm_type,
368+
osm_id=row.osm_id))
369+
result = addr_row
340370

341371
# Check for an interpolation that is either closer than our result
342372
# or belongs to a close street found.
343-
if self.max_rank > 27 and self.layer_enabled(DataLayer.ADDRESS):
373+
# No point in doing this when the result is already inside a building,
374+
# i.e. when the distance is already 0.
375+
if self.max_rank > 27 and self.layer_enabled(DataLayer.ADDRESS) and distance > 0:
344376
log().comment('Find interpolation for street')
345-
addr_row = await self._find_interpolation_for_street(parent_place_id,
346-
distance)
377+
addr_row = await self._find_interpolation_for_street(parent_street, distance)
347378
log().var_dump('Result (street interpolation)', addr_row)
348379
if addr_row is not None:
349-
row = addr_row
350-
row_func = nres.create_from_osmline_row
380+
return addr_row, nres.create_from_osmline_row
351381

352-
return row, row_func
382+
return result, row_func
353383

354384
async def _lookup_area_address(self) -> Optional[SaRow]:
355385
""" Lookup large addressable areas for the given WKT point.

test/python/api/test_api_reverse.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -163,12 +163,12 @@ def test_reverse_housenumber_interpolation(apiobj, frontend, with_geom):
163163
parent_place_id=990,
164164
rank_search=30, rank_address=30,
165165
housenumber='23',
166-
centroid=(10.0, 10.00002))
166+
centroid=(10.0, 10.0002))
167167
apiobj.add_osmline(place_id=992,
168168
parent_place_id=990,
169169
startnumber=1, endnumber=3, step=1,
170-
centroid=(10.0, 10.00001),
171-
geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
170+
centroid=(10.0, 10.0001),
171+
geometry='LINESTRING(9.995 10.0001, 10.005 10.0001)')
172172
apiobj.add_placex(place_id=1990, class_='highway', type='service',
173173
rank_search=27, rank_address=27,
174174
name={'name': 'Other Street'},
@@ -177,8 +177,8 @@ def test_reverse_housenumber_interpolation(apiobj, frontend, with_geom):
177177
apiobj.add_osmline(place_id=1992,
178178
parent_place_id=1990,
179179
startnumber=1, endnumber=3, step=1,
180-
centroid=(10.0, 20.00001),
181-
geometry='LINESTRING(9.995 20.00001, 10.005 20.00001)')
180+
centroid=(10.0, 20.0001),
181+
geometry='LINESTRING(9.995 20.0001, 10.005 20.0001)')
182182

183183
params = {'geometry_output': napi.GeometryFormat.TEXT} if with_geom else {}
184184

0 commit comments

Comments
 (0)