1 # SPDX-License-Identifier: GPL-3.0-or-later
3 # This file is part of Nominatim. (https://nominatim.org)
5 # Copyright (C) 2025 by the Nominatim developer community.
6 # For a full list of authors see the git log.
8 Tests for reverse API call.
10 These tests make sure that all Python code is correct and executable.
11 Functional tests can be found in the BDD test suite.
17 import nominatim_api as napi
19 API_OPTIONS = {'reverse'}
22 def test_reverse_rank_30(apiobj, frontend):
23 apiobj.add_placex(place_id=223, class_='place', type='house',
26 geometry='POINT(1.3 0.7)')
28 api = frontend(apiobj, options=API_OPTIONS)
29 result = api.reverse((1.3, 0.7))
31 assert result is not None
32 assert result.place_id == 223
35 @pytest.mark.parametrize('country', ['de', 'us'])
36 def test_reverse_street(apiobj, frontend, country):
37 apiobj.add_placex(place_id=990, class_='highway', type='service',
38 rank_search=27, rank_address=27,
39 name={'name': 'My Street'},
40 centroid=(10.0, 10.0),
42 geometry='LINESTRING(9.995 10, 10.005 10)')
44 api = frontend(apiobj, options=API_OPTIONS)
45 assert api.reverse((9.995, 10)).place_id == 990
48 def test_reverse_ignore_unindexed(apiobj, frontend):
49 apiobj.add_placex(place_id=223, class_='place', type='house',
53 geometry='POINT(1.3 0.7)')
55 api = frontend(apiobj, options=API_OPTIONS)
56 result = api.reverse((1.3, 0.7))
61 @pytest.mark.parametrize('y,layer,place_id',
62 [(0.7, napi.DataLayer.ADDRESS, 223),
63 (0.70001, napi.DataLayer.POI, 224),
64 (0.7, napi.DataLayer.ADDRESS | napi.DataLayer.POI, 224),
65 (0.70001, napi.DataLayer.ADDRESS | napi.DataLayer.POI, 223),
66 (0.7, napi.DataLayer.MANMADE, 225),
67 (0.7, napi.DataLayer.RAILWAY, 226),
68 (0.7, napi.DataLayer.NATURAL, 227),
69 (0.70003, napi.DataLayer.MANMADE | napi.DataLayer.RAILWAY, 225),
70 (0.70003, napi.DataLayer.MANMADE | napi.DataLayer.NATURAL, 225),
71 (5, napi.DataLayer.ADDRESS, 229),
72 (5.0001, napi.DataLayer.ADDRESS, 229)])
73 def test_reverse_rank_30_layers(apiobj, frontend, y, layer, place_id):
74 apiobj.add_placex(place_id=223, osm_type='N', class_='place', type='house',
78 centroid=(1.3, 0.70001))
79 apiobj.add_placex(place_id=224, osm_type='N', class_='amenity', type='toilet',
83 apiobj.add_placex(place_id=225, osm_type='N', class_='man_made', type='tower',
86 centroid=(1.3, 0.70003))
87 apiobj.add_placex(place_id=226, osm_type='N', class_='railway', type='station',
90 centroid=(1.3, 0.70004))
91 apiobj.add_placex(place_id=227, osm_type='N', class_='natural', type='cave',
94 centroid=(1.3, 0.70005))
95 apiobj.add_placex(place_id=229, class_='place', type='house',
96 name={'addr:housename': 'Old Cottage'},
100 apiobj.add_placex(place_id=230, class_='place', type='house',
102 address={'_inherited': ''},
105 centroid=(1.3, 5.0001))
107 api = frontend(apiobj, options=API_OPTIONS)
108 assert api.reverse((1.3, y), layers=layer).place_id == place_id
111 def test_reverse_poi_layer_with_no_pois(apiobj, frontend):
112 apiobj.add_placex(place_id=223, class_='place', type='house',
116 centroid=(1.3, 0.70001))
118 api = frontend(apiobj, options=API_OPTIONS)
119 assert api.reverse((1.3, 0.70001), max_rank=29,
120 layers=napi.DataLayer.POI) is None
123 @pytest.mark.parametrize('with_geom', [True, False])
124 def test_reverse_housenumber_on_street(apiobj, frontend, with_geom):
125 apiobj.add_placex(place_id=990, class_='highway', type='service',
126 rank_search=27, rank_address=27,
127 name={'name': 'My Street'},
128 centroid=(10.0, 10.0),
129 geometry='LINESTRING(9.995 10, 10.005 10)')
130 apiobj.add_placex(place_id=991, class_='place', type='house',
132 rank_search=30, rank_address=30,
134 centroid=(10.0, 10.00001))
135 apiobj.add_placex(place_id=1990, class_='highway', type='service',
136 rank_search=27, rank_address=27,
137 name={'name': 'Other Street'},
138 centroid=(10.0, 1.0),
139 geometry='LINESTRING(9.995 1, 10.005 1)')
140 apiobj.add_placex(place_id=1991, class_='place', type='house',
141 parent_place_id=1990,
142 rank_search=30, rank_address=30,
144 centroid=(10.0, 1.00001))
146 params = {'geometry_output': napi.GeometryFormat.TEXT} if with_geom else {}
148 api = frontend(apiobj, options=API_OPTIONS)
149 assert api.reverse((10.0, 10.0), max_rank=30, **params).place_id == 991
150 assert api.reverse((10.0, 10.0), max_rank=27).place_id == 990
151 assert api.reverse((10.0, 10.00001), max_rank=30).place_id == 991
152 assert api.reverse((10.0, 1.0), **params).place_id == 1991
155 @pytest.mark.parametrize('with_geom', [True, False])
156 def test_reverse_housenumber_interpolation(apiobj, frontend, with_geom):
157 apiobj.add_placex(place_id=990, class_='highway', type='service',
158 rank_search=27, rank_address=27,
159 name={'name': 'My Street'},
160 centroid=(10.0, 10.0),
161 geometry='LINESTRING(9.995 10, 10.005 10)')
162 apiobj.add_placex(place_id=991, class_='place', type='house',
164 rank_search=30, rank_address=30,
166 centroid=(10.0, 10.0002))
167 apiobj.add_osmline(place_id=992,
169 startnumber=1, endnumber=3, step=1,
170 centroid=(10.0, 10.0001),
171 geometry='LINESTRING(9.995 10.0001, 10.005 10.0001)')
172 apiobj.add_placex(place_id=1990, class_='highway', type='service',
173 rank_search=27, rank_address=27,
174 name={'name': 'Other Street'},
175 centroid=(10.0, 20.0),
176 geometry='LINESTRING(9.995 20, 10.005 20)')
177 apiobj.add_osmline(place_id=1992,
178 parent_place_id=1990,
179 startnumber=1, endnumber=3, step=1,
180 centroid=(10.0, 20.0001),
181 geometry='LINESTRING(9.995 20.0001, 10.005 20.0001)')
183 params = {'geometry_output': napi.GeometryFormat.TEXT} if with_geom else {}
185 api = frontend(apiobj, options=API_OPTIONS)
186 assert api.reverse((10.0, 10.0), **params).place_id == 992
187 assert api.reverse((10.0, 20.0), **params).place_id == 1992
190 def test_reverse_housenumber_point_interpolation(apiobj, frontend):
191 apiobj.add_placex(place_id=990, class_='highway', type='service',
192 rank_search=27, rank_address=27,
193 name={'name': 'My Street'},
194 centroid=(10.0, 10.0),
195 geometry='LINESTRING(9.995 10, 10.005 10)')
196 apiobj.add_osmline(place_id=992,
198 startnumber=42, endnumber=42, step=1,
199 centroid=(10.0, 10.00001),
200 geometry='POINT(10.0 10.00001)')
202 api = frontend(apiobj, options=API_OPTIONS)
203 res = api.reverse((10.0, 10.0))
204 assert res.place_id == 992
205 assert res.housenumber == '42'
208 def test_reverse_tiger_number(apiobj, frontend):
209 apiobj.add_placex(place_id=990, osm_type='W', osm_id=100,
210 class_='highway', type='service',
211 rank_search=27, rank_address=27,
212 name={'name': 'My Street'},
213 centroid=(10.0, 10.0),
215 geometry='LINESTRING(9.995 10, 10.005 10)')
216 apiobj.add_placex(place_id=991, osm_type='N', osm_id=5,
217 class_='place', type='house',
218 rank_search=30, rank_address=30,
221 centroid=(10.0, 10.0007),
223 apiobj.add_tiger(place_id=992,
225 startnumber=1, endnumber=3, step=1,
226 centroid=(10.0, 10.00001),
227 geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
229 api = frontend(apiobj, options=API_OPTIONS)
231 result = api.reverse((10.0, 10.0))
232 assert result.place_id == 992
233 assert result.osm_object == ('W', 100)
235 assert api.reverse((10.0, 10.00001)).place_id == 992
238 def test_reverse_point_tiger(apiobj, frontend):
239 apiobj.add_placex(place_id=990, class_='highway', type='service',
240 rank_search=27, rank_address=27,
241 name={'name': 'My Street'},
242 centroid=(10.0, 10.0),
244 geometry='LINESTRING(9.995 10, 10.005 10)')
245 apiobj.add_tiger(place_id=992,
247 startnumber=1, endnumber=1, step=1,
248 centroid=(10.0, 10.00001),
249 geometry='POINT(10.0 10.00001)')
251 api = frontend(apiobj, options=API_OPTIONS)
252 res = api.reverse((10.0, 10.0))
253 assert res.place_id == 992
254 assert res.housenumber == '1'
257 def test_reverse_low_zoom_address(apiobj, frontend):
258 apiobj.add_placex(place_id=1001, class_='place', type='house',
262 centroid=(59.3, 80.70001))
263 apiobj.add_placex(place_id=1002, class_='place', type='town',
264 name={'name': 'Town'},
267 centroid=(59.3, 80.70001),
268 geometry="""POLYGON((59.3 80.70001, 59.3001 80.70001,
269 59.3001 80.70101, 59.3 80.70101, 59.3 80.70001))""")
271 api = frontend(apiobj, options=API_OPTIONS)
272 assert api.reverse((59.30005, 80.7005)).place_id == 1001
273 assert api.reverse((59.30005, 80.7005), max_rank=18).place_id == 1002
276 def test_reverse_place_node_in_area(apiobj, frontend):
277 apiobj.add_placex(place_id=1002, class_='place', type='town',
278 name={'name': 'Town Area'},
281 centroid=(59.3, 80.70001),
282 geometry="""POLYGON((59.3 80.70001, 59.3001 80.70001,
283 59.3001 80.70101, 59.3 80.70101, 59.3 80.70001))""")
284 apiobj.add_placex(place_id=1003, class_='place', type='suburb',
285 name={'name': 'Suburb Point'},
289 centroid=(59.30004, 80.70055))
291 api = frontend(apiobj, options=API_OPTIONS)
292 assert api.reverse((59.30004, 80.70055)).place_id == 1003
295 @pytest.mark.parametrize('layer,place_id', [(napi.DataLayer.MANMADE, 225),
296 (napi.DataLayer.RAILWAY, 226),
297 (napi.DataLayer.NATURAL, 227),
298 (napi.DataLayer.MANMADE | napi.DataLayer.RAILWAY, 225),
299 (napi.DataLayer.MANMADE | napi.DataLayer.NATURAL, 225)])
300 def test_reverse_larger_area_layers(apiobj, frontend, layer, place_id):
301 apiobj.add_placex(place_id=225, class_='man_made', type='dam',
302 name={'name': 'Dam'},
305 centroid=(1.3, 0.70003))
306 apiobj.add_placex(place_id=226, class_='railway', type='yard',
307 name={'name': 'Dam'},
310 centroid=(1.3, 0.70004))
311 apiobj.add_placex(place_id=227, class_='natural', type='spring',
312 name={'name': 'Dam'},
315 centroid=(1.3, 0.70005))
317 api = frontend(apiobj, options=API_OPTIONS)
318 assert api.reverse((1.3, 0.7), layers=layer).place_id == place_id
321 def test_reverse_country_lookup_no_objects(apiobj, frontend):
322 apiobj.add_country('xx', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))')
324 api = frontend(apiobj, options=API_OPTIONS)
325 assert api.reverse((0.5, 0.5)) is None
328 @pytest.mark.parametrize('rank', [4, 30])
329 @pytest.mark.parametrize('with_geom', [True, False])
330 def test_reverse_country_lookup_country_only(apiobj, frontend, rank, with_geom):
331 apiobj.add_country('xx', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))')
332 apiobj.add_country('yy', 'POLYGON((10 0, 10 1, 11 1, 11 0, 10 0))')
333 apiobj.add_placex(place_id=225, class_='place', type='country',
334 name={'name': 'My Country'},
340 params = {'max_rank': rank}
342 params['geometry_output'] = napi.GeometryFormat.TEXT
344 api = frontend(apiobj, options=API_OPTIONS)
345 assert api.reverse((0.5, 0.5), **params).place_id == 225
346 assert api.reverse((10.5, 0.5), **params) is None
349 @pytest.mark.parametrize('with_geom', [True, False])
350 def test_reverse_country_lookup_place_node_inside(apiobj, frontend, with_geom):
351 apiobj.add_country('xx', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))')
352 apiobj.add_country('yy', 'POLYGON((10 0, 10 1, 11 1, 11 0, 10 0))')
353 apiobj.add_placex(place_id=225, class_='place', type='state',
355 name={'name': 'My State'},
359 centroid=(0.5, 0.505))
360 apiobj.add_placex(place_id=425, class_='place', type='state',
362 name={'name': 'Other State'},
366 centroid=(10.5, 0.505))
368 params = {'geometry_output': napi.GeometryFormat.KML} if with_geom else {}
370 api = frontend(apiobj, options=API_OPTIONS)
371 assert api.reverse((0.5, 0.5), **params).place_id == 225
372 assert api.reverse((10.5, 0.5), **params).place_id == 425
375 @pytest.mark.parametrize('gtype', list(napi.GeometryFormat))
376 def test_reverse_geometry_output_placex(apiobj, frontend, gtype):
377 apiobj.add_country('xx', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))')
378 apiobj.add_placex(place_id=1001, class_='place', type='house',
382 centroid=(59.3, 80.70001))
383 apiobj.add_placex(place_id=1003, class_='place', type='suburb',
384 name={'name': 'Suburb Point'},
391 api = frontend(apiobj, options=API_OPTIONS)
392 assert api.reverse((59.3, 80.70001), geometry_output=gtype).place_id == 1001
393 assert api.reverse((0.5, 0.5), geometry_output=gtype).place_id == 1003
396 def test_reverse_simplified_geometry(apiobj, frontend):
397 apiobj.add_placex(place_id=1001, class_='place', type='house',
401 centroid=(59.3, 80.70001))
403 api = frontend(apiobj, options=API_OPTIONS)
404 details = dict(geometry_output=napi.GeometryFormat.GEOJSON,
405 geometry_simplification=0.1)
406 assert api.reverse((59.3, 80.70001), **details).place_id == 1001
409 def test_reverse_interpolation_geometry(apiobj, frontend):
410 apiobj.add_osmline(place_id=992,
412 startnumber=1, endnumber=3, step=1,
413 centroid=(10.0, 10.00001),
414 geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
416 api = frontend(apiobj, options=API_OPTIONS)
417 result = api.reverse((10.0, 10.0), geometry_output=napi.GeometryFormat.TEXT)
419 assert result.geometry['text'] == 'POINT(10 10.00001)'
422 def test_reverse_tiger_geometry(apiobj, frontend):
423 apiobj.add_placex(place_id=990, class_='highway', type='service',
424 rank_search=27, rank_address=27,
425 name={'name': 'My Street'},
426 centroid=(10.0, 10.0),
428 geometry='LINESTRING(9.995 10, 10.005 10)')
429 apiobj.add_tiger(place_id=992,
431 startnumber=1, endnumber=3, step=1,
432 centroid=(10.0, 10.00001),
433 geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
434 apiobj.add_placex(place_id=1000, class_='highway', type='service',
435 rank_search=27, rank_address=27,
436 name={'name': 'My Street'},
437 centroid=(11.0, 11.0),
439 geometry='LINESTRING(10.995 11, 11.005 11)')
440 apiobj.add_tiger(place_id=1001,
441 parent_place_id=1000,
442 startnumber=1, endnumber=3, step=1,
443 centroid=(11.0, 11.00001),
444 geometry='LINESTRING(10.995 11.00001, 11.005 11.00001)')
446 api = frontend(apiobj, options=API_OPTIONS)
448 params = {'geometry_output': napi.GeometryFormat.GEOJSON}
450 output = api.reverse((10.0, 10.0), **params)
451 assert json.loads(output.geometry['geojson']) \
452 == {'coordinates': [10, 10.00001], 'type': 'Point'}
454 output = api.reverse((11.0, 11.0), **params)
455 assert json.loads(output.geometry['geojson']) \
456 == {'coordinates': [11, 11.00001], 'type': 'Point'}