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.00002))
167 apiobj.add_osmline(place_id=992,
169 startnumber=1, endnumber=3, step=1,
170 centroid=(10.0, 10.00001),
171 geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
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.00001),
181 geometry='LINESTRING(9.995 20.00001, 10.005 20.00001)')
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, class_='highway', type='service',
210 rank_search=27, rank_address=27,
211 name={'name': 'My Street'},
212 centroid=(10.0, 10.0),
214 geometry='LINESTRING(9.995 10, 10.005 10)')
215 apiobj.add_tiger(place_id=992,
217 startnumber=1, endnumber=3, step=1,
218 centroid=(10.0, 10.00001),
219 geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
221 api = frontend(apiobj, options=API_OPTIONS)
222 assert api.reverse((10.0, 10.0)).place_id == 992
223 assert api.reverse((10.0, 10.00001)).place_id == 992
226 def test_reverse_point_tiger(apiobj, frontend):
227 apiobj.add_placex(place_id=990, class_='highway', type='service',
228 rank_search=27, rank_address=27,
229 name={'name': 'My Street'},
230 centroid=(10.0, 10.0),
232 geometry='LINESTRING(9.995 10, 10.005 10)')
233 apiobj.add_tiger(place_id=992,
235 startnumber=1, endnumber=1, step=1,
236 centroid=(10.0, 10.00001),
237 geometry='POINT(10.0 10.00001)')
239 api = frontend(apiobj, options=API_OPTIONS)
240 res = api.reverse((10.0, 10.0))
241 assert res.place_id == 992
242 assert res.housenumber == '1'
245 def test_reverse_low_zoom_address(apiobj, frontend):
246 apiobj.add_placex(place_id=1001, class_='place', type='house',
250 centroid=(59.3, 80.70001))
251 apiobj.add_placex(place_id=1002, class_='place', type='town',
252 name={'name': 'Town'},
255 centroid=(59.3, 80.70001),
256 geometry="""POLYGON((59.3 80.70001, 59.3001 80.70001,
257 59.3001 80.70101, 59.3 80.70101, 59.3 80.70001))""")
259 api = frontend(apiobj, options=API_OPTIONS)
260 assert api.reverse((59.30005, 80.7005)).place_id == 1001
261 assert api.reverse((59.30005, 80.7005), max_rank=18).place_id == 1002
264 def test_reverse_place_node_in_area(apiobj, frontend):
265 apiobj.add_placex(place_id=1002, class_='place', type='town',
266 name={'name': 'Town Area'},
269 centroid=(59.3, 80.70001),
270 geometry="""POLYGON((59.3 80.70001, 59.3001 80.70001,
271 59.3001 80.70101, 59.3 80.70101, 59.3 80.70001))""")
272 apiobj.add_placex(place_id=1003, class_='place', type='suburb',
273 name={'name': 'Suburb Point'},
277 centroid=(59.30004, 80.70055))
279 api = frontend(apiobj, options=API_OPTIONS)
280 assert api.reverse((59.30004, 80.70055)).place_id == 1003
283 @pytest.mark.parametrize('layer,place_id', [(napi.DataLayer.MANMADE, 225),
284 (napi.DataLayer.RAILWAY, 226),
285 (napi.DataLayer.NATURAL, 227),
286 (napi.DataLayer.MANMADE | napi.DataLayer.RAILWAY, 225),
287 (napi.DataLayer.MANMADE | napi.DataLayer.NATURAL, 225)])
288 def test_reverse_larger_area_layers(apiobj, frontend, layer, place_id):
289 apiobj.add_placex(place_id=225, class_='man_made', type='dam',
290 name={'name': 'Dam'},
293 centroid=(1.3, 0.70003))
294 apiobj.add_placex(place_id=226, class_='railway', type='yard',
295 name={'name': 'Dam'},
298 centroid=(1.3, 0.70004))
299 apiobj.add_placex(place_id=227, class_='natural', type='spring',
300 name={'name': 'Dam'},
303 centroid=(1.3, 0.70005))
305 api = frontend(apiobj, options=API_OPTIONS)
306 assert api.reverse((1.3, 0.7), layers=layer).place_id == place_id
309 def test_reverse_country_lookup_no_objects(apiobj, frontend):
310 apiobj.add_country('xx', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))')
312 api = frontend(apiobj, options=API_OPTIONS)
313 assert api.reverse((0.5, 0.5)) is None
316 @pytest.mark.parametrize('rank', [4, 30])
317 @pytest.mark.parametrize('with_geom', [True, False])
318 def test_reverse_country_lookup_country_only(apiobj, frontend, rank, with_geom):
319 apiobj.add_country('xx', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))')
320 apiobj.add_country('yy', 'POLYGON((10 0, 10 1, 11 1, 11 0, 10 0))')
321 apiobj.add_placex(place_id=225, class_='place', type='country',
322 name={'name': 'My Country'},
328 params = {'max_rank': rank}
330 params['geometry_output'] = napi.GeometryFormat.TEXT
332 api = frontend(apiobj, options=API_OPTIONS)
333 assert api.reverse((0.5, 0.5), **params).place_id == 225
334 assert api.reverse((10.5, 0.5), **params) is None
337 @pytest.mark.parametrize('with_geom', [True, False])
338 def test_reverse_country_lookup_place_node_inside(apiobj, frontend, with_geom):
339 apiobj.add_country('xx', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))')
340 apiobj.add_country('yy', 'POLYGON((10 0, 10 1, 11 1, 11 0, 10 0))')
341 apiobj.add_placex(place_id=225, class_='place', type='state',
343 name={'name': 'My State'},
347 centroid=(0.5, 0.505))
348 apiobj.add_placex(place_id=425, class_='place', type='state',
350 name={'name': 'Other State'},
354 centroid=(10.5, 0.505))
356 params = {'geometry_output': napi.GeometryFormat.KML} if with_geom else {}
358 api = frontend(apiobj, options=API_OPTIONS)
359 assert api.reverse((0.5, 0.5), **params).place_id == 225
360 assert api.reverse((10.5, 0.5), **params).place_id == 425
363 @pytest.mark.parametrize('gtype', list(napi.GeometryFormat))
364 def test_reverse_geometry_output_placex(apiobj, frontend, gtype):
365 apiobj.add_country('xx', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))')
366 apiobj.add_placex(place_id=1001, class_='place', type='house',
370 centroid=(59.3, 80.70001))
371 apiobj.add_placex(place_id=1003, class_='place', type='suburb',
372 name={'name': 'Suburb Point'},
379 api = frontend(apiobj, options=API_OPTIONS)
380 assert api.reverse((59.3, 80.70001), geometry_output=gtype).place_id == 1001
381 assert api.reverse((0.5, 0.5), geometry_output=gtype).place_id == 1003
384 def test_reverse_simplified_geometry(apiobj, frontend):
385 apiobj.add_placex(place_id=1001, class_='place', type='house',
389 centroid=(59.3, 80.70001))
391 api = frontend(apiobj, options=API_OPTIONS)
392 details = dict(geometry_output=napi.GeometryFormat.GEOJSON,
393 geometry_simplification=0.1)
394 assert api.reverse((59.3, 80.70001), **details).place_id == 1001
397 def test_reverse_interpolation_geometry(apiobj, frontend):
398 apiobj.add_osmline(place_id=992,
400 startnumber=1, endnumber=3, step=1,
401 centroid=(10.0, 10.00001),
402 geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
404 api = frontend(apiobj, options=API_OPTIONS)
405 result = api.reverse((10.0, 10.0), geometry_output=napi.GeometryFormat.TEXT)
407 assert result.geometry['text'] == 'POINT(10 10.00001)'
410 def test_reverse_tiger_geometry(apiobj, frontend):
411 apiobj.add_placex(place_id=990, class_='highway', type='service',
412 rank_search=27, rank_address=27,
413 name={'name': 'My Street'},
414 centroid=(10.0, 10.0),
416 geometry='LINESTRING(9.995 10, 10.005 10)')
417 apiobj.add_tiger(place_id=992,
419 startnumber=1, endnumber=3, step=1,
420 centroid=(10.0, 10.00001),
421 geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
422 apiobj.add_placex(place_id=1000, class_='highway', type='service',
423 rank_search=27, rank_address=27,
424 name={'name': 'My Street'},
425 centroid=(11.0, 11.0),
427 geometry='LINESTRING(10.995 11, 11.005 11)')
428 apiobj.add_tiger(place_id=1001,
429 parent_place_id=1000,
430 startnumber=1, endnumber=3, step=1,
431 centroid=(11.0, 11.00001),
432 geometry='LINESTRING(10.995 11.00001, 11.005 11.00001)')
434 api = frontend(apiobj, options=API_OPTIONS)
436 params = {'geometry_output': napi.GeometryFormat.GEOJSON}
438 output = api.reverse((10.0, 10.0), **params)
439 assert json.loads(output.geometry['geojson']) \
440 == {'coordinates': [10, 10.00001], 'type': 'Point'}
442 output = api.reverse((11.0, 11.0), **params)
443 assert json.loads(output.geometry['geojson']) \
444 == {'coordinates': [11, 11.00001], 'type': 'Point'}