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 def test_reverse_rank_30_layers(apiobj, frontend, y, layer, place_id):
73 apiobj.add_placex(place_id=223, osm_type='N', class_='place', type='house',
77 centroid=(1.3, 0.70001))
78 apiobj.add_placex(place_id=224, osm_type='N', class_='amenity', type='toilet',
82 apiobj.add_placex(place_id=225, osm_type='N', class_='man_made', type='tower',
85 centroid=(1.3, 0.70003))
86 apiobj.add_placex(place_id=226, osm_type='N', class_='railway', type='station',
89 centroid=(1.3, 0.70004))
90 apiobj.add_placex(place_id=227, osm_type='N', class_='natural', type='cave',
93 centroid=(1.3, 0.70005))
94 apiobj.add_placex(place_id=229, class_='place', type='house',
95 name={'addr:housename': 'Old Cottage'},
100 api = frontend(apiobj, options=API_OPTIONS)
101 assert api.reverse((1.3, y), layers=layer).place_id == place_id
104 def test_reverse_poi_layer_with_no_pois(apiobj, frontend):
105 apiobj.add_placex(place_id=223, class_='place', type='house',
109 centroid=(1.3, 0.70001))
111 api = frontend(apiobj, options=API_OPTIONS)
112 assert api.reverse((1.3, 0.70001), max_rank=29,
113 layers=napi.DataLayer.POI) is None
116 @pytest.mark.parametrize('with_geom', [True, False])
117 def test_reverse_housenumber_on_street(apiobj, frontend, with_geom):
118 apiobj.add_placex(place_id=990, class_='highway', type='service',
119 rank_search=27, rank_address=27,
120 name={'name': 'My Street'},
121 centroid=(10.0, 10.0),
122 geometry='LINESTRING(9.995 10, 10.005 10)')
123 apiobj.add_placex(place_id=991, class_='place', type='house',
125 rank_search=30, rank_address=30,
127 centroid=(10.0, 10.00001))
128 apiobj.add_placex(place_id=1990, class_='highway', type='service',
129 rank_search=27, rank_address=27,
130 name={'name': 'Other Street'},
131 centroid=(10.0, 1.0),
132 geometry='LINESTRING(9.995 1, 10.005 1)')
133 apiobj.add_placex(place_id=1991, class_='place', type='house',
134 parent_place_id=1990,
135 rank_search=30, rank_address=30,
137 centroid=(10.0, 1.00001))
139 params = {'geometry_output': napi.GeometryFormat.TEXT} if with_geom else {}
141 api = frontend(apiobj, options=API_OPTIONS)
142 assert api.reverse((10.0, 10.0), max_rank=30, **params).place_id == 991
143 assert api.reverse((10.0, 10.0), max_rank=27).place_id == 990
144 assert api.reverse((10.0, 10.00001), max_rank=30).place_id == 991
145 assert api.reverse((10.0, 1.0), **params).place_id == 1991
148 @pytest.mark.parametrize('with_geom', [True, False])
149 def test_reverse_housenumber_interpolation(apiobj, frontend, with_geom):
150 apiobj.add_placex(place_id=990, class_='highway', type='service',
151 rank_search=27, rank_address=27,
152 name={'name': 'My Street'},
153 centroid=(10.0, 10.0),
154 geometry='LINESTRING(9.995 10, 10.005 10)')
155 apiobj.add_placex(place_id=991, class_='place', type='house',
157 rank_search=30, rank_address=30,
159 centroid=(10.0, 10.00002))
160 apiobj.add_osmline(place_id=992,
162 startnumber=1, endnumber=3, step=1,
163 centroid=(10.0, 10.00001),
164 geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
165 apiobj.add_placex(place_id=1990, class_='highway', type='service',
166 rank_search=27, rank_address=27,
167 name={'name': 'Other Street'},
168 centroid=(10.0, 20.0),
169 geometry='LINESTRING(9.995 20, 10.005 20)')
170 apiobj.add_osmline(place_id=1992,
171 parent_place_id=1990,
172 startnumber=1, endnumber=3, step=1,
173 centroid=(10.0, 20.00001),
174 geometry='LINESTRING(9.995 20.00001, 10.005 20.00001)')
176 params = {'geometry_output': napi.GeometryFormat.TEXT} if with_geom else {}
178 api = frontend(apiobj, options=API_OPTIONS)
179 assert api.reverse((10.0, 10.0), **params).place_id == 992
180 assert api.reverse((10.0, 20.0), **params).place_id == 1992
183 def test_reverse_housenumber_point_interpolation(apiobj, frontend):
184 apiobj.add_placex(place_id=990, class_='highway', type='service',
185 rank_search=27, rank_address=27,
186 name={'name': 'My Street'},
187 centroid=(10.0, 10.0),
188 geometry='LINESTRING(9.995 10, 10.005 10)')
189 apiobj.add_osmline(place_id=992,
191 startnumber=42, endnumber=42, step=1,
192 centroid=(10.0, 10.00001),
193 geometry='POINT(10.0 10.00001)')
195 api = frontend(apiobj, options=API_OPTIONS)
196 res = api.reverse((10.0, 10.0))
197 assert res.place_id == 992
198 assert res.housenumber == '42'
201 def test_reverse_tiger_number(apiobj, frontend):
202 apiobj.add_placex(place_id=990, class_='highway', type='service',
203 rank_search=27, rank_address=27,
204 name={'name': 'My Street'},
205 centroid=(10.0, 10.0),
207 geometry='LINESTRING(9.995 10, 10.005 10)')
208 apiobj.add_tiger(place_id=992,
210 startnumber=1, endnumber=3, step=1,
211 centroid=(10.0, 10.00001),
212 geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
214 api = frontend(apiobj, options=API_OPTIONS)
215 assert api.reverse((10.0, 10.0)).place_id == 992
216 assert api.reverse((10.0, 10.00001)).place_id == 992
219 def test_reverse_point_tiger(apiobj, frontend):
220 apiobj.add_placex(place_id=990, class_='highway', type='service',
221 rank_search=27, rank_address=27,
222 name={'name': 'My Street'},
223 centroid=(10.0, 10.0),
225 geometry='LINESTRING(9.995 10, 10.005 10)')
226 apiobj.add_tiger(place_id=992,
228 startnumber=1, endnumber=1, step=1,
229 centroid=(10.0, 10.00001),
230 geometry='POINT(10.0 10.00001)')
232 api = frontend(apiobj, options=API_OPTIONS)
233 res = api.reverse((10.0, 10.0))
234 assert res.place_id == 992
235 assert res.housenumber == '1'
238 def test_reverse_low_zoom_address(apiobj, frontend):
239 apiobj.add_placex(place_id=1001, class_='place', type='house',
243 centroid=(59.3, 80.70001))
244 apiobj.add_placex(place_id=1002, class_='place', type='town',
245 name={'name': 'Town'},
248 centroid=(59.3, 80.70001),
249 geometry="""POLYGON((59.3 80.70001, 59.3001 80.70001,
250 59.3001 80.70101, 59.3 80.70101, 59.3 80.70001))""")
252 api = frontend(apiobj, options=API_OPTIONS)
253 assert api.reverse((59.30005, 80.7005)).place_id == 1001
254 assert api.reverse((59.30005, 80.7005), max_rank=18).place_id == 1002
257 def test_reverse_place_node_in_area(apiobj, frontend):
258 apiobj.add_placex(place_id=1002, class_='place', type='town',
259 name={'name': 'Town Area'},
262 centroid=(59.3, 80.70001),
263 geometry="""POLYGON((59.3 80.70001, 59.3001 80.70001,
264 59.3001 80.70101, 59.3 80.70101, 59.3 80.70001))""")
265 apiobj.add_placex(place_id=1003, class_='place', type='suburb',
266 name={'name': 'Suburb Point'},
270 centroid=(59.30004, 80.70055))
272 api = frontend(apiobj, options=API_OPTIONS)
273 assert api.reverse((59.30004, 80.70055)).place_id == 1003
276 @pytest.mark.parametrize('layer,place_id', [(napi.DataLayer.MANMADE, 225),
277 (napi.DataLayer.RAILWAY, 226),
278 (napi.DataLayer.NATURAL, 227),
279 (napi.DataLayer.MANMADE | napi.DataLayer.RAILWAY, 225),
280 (napi.DataLayer.MANMADE | napi.DataLayer.NATURAL, 225)])
281 def test_reverse_larger_area_layers(apiobj, frontend, layer, place_id):
282 apiobj.add_placex(place_id=225, class_='man_made', type='dam',
283 name={'name': 'Dam'},
286 centroid=(1.3, 0.70003))
287 apiobj.add_placex(place_id=226, class_='railway', type='yard',
288 name={'name': 'Dam'},
291 centroid=(1.3, 0.70004))
292 apiobj.add_placex(place_id=227, class_='natural', type='spring',
293 name={'name': 'Dam'},
296 centroid=(1.3, 0.70005))
298 api = frontend(apiobj, options=API_OPTIONS)
299 assert api.reverse((1.3, 0.7), layers=layer).place_id == place_id
302 def test_reverse_country_lookup_no_objects(apiobj, frontend):
303 apiobj.add_country('xx', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))')
305 api = frontend(apiobj, options=API_OPTIONS)
306 assert api.reverse((0.5, 0.5)) is None
309 @pytest.mark.parametrize('rank', [4, 30])
310 @pytest.mark.parametrize('with_geom', [True, False])
311 def test_reverse_country_lookup_country_only(apiobj, frontend, rank, with_geom):
312 apiobj.add_country('xx', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))')
313 apiobj.add_country('yy', 'POLYGON((10 0, 10 1, 11 1, 11 0, 10 0))')
314 apiobj.add_placex(place_id=225, class_='place', type='country',
315 name={'name': 'My Country'},
321 params = {'max_rank': rank}
323 params['geometry_output'] = napi.GeometryFormat.TEXT
325 api = frontend(apiobj, options=API_OPTIONS)
326 assert api.reverse((0.5, 0.5), **params).place_id == 225
327 assert api.reverse((10.5, 0.5), **params) is None
330 @pytest.mark.parametrize('with_geom', [True, False])
331 def test_reverse_country_lookup_place_node_inside(apiobj, frontend, with_geom):
332 apiobj.add_country('xx', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))')
333 apiobj.add_country('yy', 'POLYGON((10 0, 10 1, 11 1, 11 0, 10 0))')
334 apiobj.add_placex(place_id=225, class_='place', type='state',
336 name={'name': 'My State'},
340 centroid=(0.5, 0.505))
341 apiobj.add_placex(place_id=425, class_='place', type='state',
343 name={'name': 'Other State'},
347 centroid=(10.5, 0.505))
349 params = {'geometry_output': napi.GeometryFormat.KML} if with_geom else {}
351 api = frontend(apiobj, options=API_OPTIONS)
352 assert api.reverse((0.5, 0.5), **params).place_id == 225
353 assert api.reverse((10.5, 0.5), **params).place_id == 425
356 @pytest.mark.parametrize('gtype', list(napi.GeometryFormat))
357 def test_reverse_geometry_output_placex(apiobj, frontend, gtype):
358 apiobj.add_country('xx', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))')
359 apiobj.add_placex(place_id=1001, class_='place', type='house',
363 centroid=(59.3, 80.70001))
364 apiobj.add_placex(place_id=1003, class_='place', type='suburb',
365 name={'name': 'Suburb Point'},
372 api = frontend(apiobj, options=API_OPTIONS)
373 assert api.reverse((59.3, 80.70001), geometry_output=gtype).place_id == 1001
374 assert api.reverse((0.5, 0.5), geometry_output=gtype).place_id == 1003
377 def test_reverse_simplified_geometry(apiobj, frontend):
378 apiobj.add_placex(place_id=1001, class_='place', type='house',
382 centroid=(59.3, 80.70001))
384 api = frontend(apiobj, options=API_OPTIONS)
385 details = dict(geometry_output=napi.GeometryFormat.GEOJSON,
386 geometry_simplification=0.1)
387 assert api.reverse((59.3, 80.70001), **details).place_id == 1001
390 def test_reverse_interpolation_geometry(apiobj, frontend):
391 apiobj.add_osmline(place_id=992,
393 startnumber=1, endnumber=3, step=1,
394 centroid=(10.0, 10.00001),
395 geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
397 api = frontend(apiobj, options=API_OPTIONS)
398 result = api.reverse((10.0, 10.0), geometry_output=napi.GeometryFormat.TEXT)
400 assert result.geometry['text'] == 'POINT(10 10.00001)'
403 def test_reverse_tiger_geometry(apiobj, frontend):
404 apiobj.add_placex(place_id=990, class_='highway', type='service',
405 rank_search=27, rank_address=27,
406 name={'name': 'My Street'},
407 centroid=(10.0, 10.0),
409 geometry='LINESTRING(9.995 10, 10.005 10)')
410 apiobj.add_tiger(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)')
415 apiobj.add_placex(place_id=1000, class_='highway', type='service',
416 rank_search=27, rank_address=27,
417 name={'name': 'My Street'},
418 centroid=(11.0, 11.0),
420 geometry='LINESTRING(10.995 11, 11.005 11)')
421 apiobj.add_tiger(place_id=1001,
422 parent_place_id=1000,
423 startnumber=1, endnumber=3, step=1,
424 centroid=(11.0, 11.00001),
425 geometry='LINESTRING(10.995 11.00001, 11.005 11.00001)')
427 api = frontend(apiobj, options=API_OPTIONS)
429 params = {'geometry_output': napi.GeometryFormat.GEOJSON}
431 output = api.reverse((10.0, 10.0), **params)
432 assert json.loads(output.geometry['geojson']) \
433 == {'coordinates': [10, 10.00001], 'type': 'Point'}
435 output = api.reverse((11.0, 11.0), **params)
436 assert json.loads(output.geometry['geojson']) \
437 == {'coordinates': [11, 11.00001], 'type': 'Point'}