]> git.openstreetmap.org Git - nominatim.git/blob - test/python/api/test_api_reverse.py
fix reported parent street for TIGER results in reverse search
[nominatim.git] / test / python / api / test_api_reverse.py
1 # SPDX-License-Identifier: GPL-3.0-or-later
2 #
3 # This file is part of Nominatim. (https://nominatim.org)
4 #
5 # Copyright (C) 2025 by the Nominatim developer community.
6 # For a full list of authors see the git log.
7 """
8 Tests for reverse API call.
9
10 These tests make sure that all Python code is correct and executable.
11 Functional tests can be found in the BDD test suite.
12 """
13 import json
14
15 import pytest
16
17 import nominatim_api as napi
18
19 API_OPTIONS = {'reverse'}
20
21
22 def test_reverse_rank_30(apiobj, frontend):
23     apiobj.add_placex(place_id=223, class_='place', type='house',
24                       housenumber='1',
25                       centroid=(1.3, 0.7),
26                       geometry='POINT(1.3 0.7)')
27
28     api = frontend(apiobj, options=API_OPTIONS)
29     result = api.reverse((1.3, 0.7))
30
31     assert result is not None
32     assert result.place_id == 223
33
34
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),
41                       country_code=country,
42                       geometry='LINESTRING(9.995 10, 10.005 10)')
43
44     api = frontend(apiobj, options=API_OPTIONS)
45     assert api.reverse((9.995, 10)).place_id == 990
46
47
48 def test_reverse_ignore_unindexed(apiobj, frontend):
49     apiobj.add_placex(place_id=223, class_='place', type='house',
50                       housenumber='1',
51                       indexed_status=2,
52                       centroid=(1.3, 0.7),
53                       geometry='POINT(1.3 0.7)')
54
55     api = frontend(apiobj, options=API_OPTIONS)
56     result = api.reverse((1.3, 0.7))
57
58     assert result is None
59
60
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',
75                       housenumber='1',
76                       rank_address=30,
77                       rank_search=30,
78                       centroid=(1.3, 0.70001))
79     apiobj.add_placex(place_id=224, osm_type='N', class_='amenity', type='toilet',
80                       rank_address=30,
81                       rank_search=30,
82                       centroid=(1.3, 0.7))
83     apiobj.add_placex(place_id=225, osm_type='N', class_='man_made', type='tower',
84                       rank_address=0,
85                       rank_search=30,
86                       centroid=(1.3, 0.70003))
87     apiobj.add_placex(place_id=226, osm_type='N', class_='railway', type='station',
88                       rank_address=0,
89                       rank_search=30,
90                       centroid=(1.3, 0.70004))
91     apiobj.add_placex(place_id=227, osm_type='N', class_='natural', type='cave',
92                       rank_address=0,
93                       rank_search=30,
94                       centroid=(1.3, 0.70005))
95     apiobj.add_placex(place_id=229, class_='place', type='house',
96                       name={'addr:housename': 'Old Cottage'},
97                       rank_address=30,
98                       rank_search=30,
99                       centroid=(1.3, 5))
100     apiobj.add_placex(place_id=230, class_='place', type='house',
101                       housenumber='2',
102                       address={'_inherited': ''},
103                       rank_address=30,
104                       rank_search=30,
105                       centroid=(1.3, 5.0001))
106
107     api = frontend(apiobj, options=API_OPTIONS)
108     assert api.reverse((1.3, y), layers=layer).place_id == place_id
109
110
111 def test_reverse_poi_layer_with_no_pois(apiobj, frontend):
112     apiobj.add_placex(place_id=223, class_='place', type='house',
113                       housenumber='1',
114                       rank_address=30,
115                       rank_search=30,
116                       centroid=(1.3, 0.70001))
117
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
121
122
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',
131                       parent_place_id=990,
132                       rank_search=30, rank_address=30,
133                       housenumber='23',
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,
143                       housenumber='23',
144                       centroid=(10.0, 1.00001))
145
146     params = {'geometry_output': napi.GeometryFormat.TEXT} if with_geom else {}
147
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
153
154
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',
163                       parent_place_id=990,
164                       rank_search=30, rank_address=30,
165                       housenumber='23',
166                       centroid=(10.0, 10.0002))
167     apiobj.add_osmline(place_id=992,
168                        parent_place_id=990,
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)')
182
183     params = {'geometry_output': napi.GeometryFormat.TEXT} if with_geom else {}
184
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
188
189
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,
197                        parent_place_id=990,
198                        startnumber=42, endnumber=42, step=1,
199                        centroid=(10.0, 10.00001),
200                        geometry='POINT(10.0 10.00001)')
201
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'
206
207
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),
214                       country_code='us',
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,
219                       housenumber='28',
220                       parent_place_id=993,
221                       centroid=(10.0, 10.0007),
222                       country_code='us')
223     apiobj.add_tiger(place_id=992,
224                      parent_place_id=990,
225                      startnumber=1, endnumber=3, step=1,
226                      centroid=(10.0, 10.00001),
227                      geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
228
229     api = frontend(apiobj, options=API_OPTIONS)
230
231     result = api.reverse((10.0, 10.0))
232     assert result.place_id == 992
233     assert result.osm_object == ('W', 100)
234
235     assert api.reverse((10.0, 10.00001)).place_id == 992
236
237
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),
243                       country_code='us',
244                       geometry='LINESTRING(9.995 10, 10.005 10)')
245     apiobj.add_tiger(place_id=992,
246                      parent_place_id=990,
247                      startnumber=1, endnumber=1, step=1,
248                      centroid=(10.0, 10.00001),
249                      geometry='POINT(10.0 10.00001)')
250
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'
255
256
257 def test_reverse_low_zoom_address(apiobj, frontend):
258     apiobj.add_placex(place_id=1001, class_='place', type='house',
259                       housenumber='1',
260                       rank_address=30,
261                       rank_search=30,
262                       centroid=(59.3, 80.70001))
263     apiobj.add_placex(place_id=1002, class_='place', type='town',
264                       name={'name': 'Town'},
265                       rank_address=16,
266                       rank_search=16,
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))""")
270
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
274
275
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'},
279                       rank_address=16,
280                       rank_search=16,
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'},
286                       osm_type='N',
287                       rank_address=18,
288                       rank_search=18,
289                       centroid=(59.30004, 80.70055))
290
291     api = frontend(apiobj, options=API_OPTIONS)
292     assert api.reverse((59.30004, 80.70055)).place_id == 1003
293
294
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'},
303                       rank_address=0,
304                       rank_search=25,
305                       centroid=(1.3, 0.70003))
306     apiobj.add_placex(place_id=226, class_='railway', type='yard',
307                       name={'name': 'Dam'},
308                       rank_address=0,
309                       rank_search=20,
310                       centroid=(1.3, 0.70004))
311     apiobj.add_placex(place_id=227, class_='natural', type='spring',
312                       name={'name': 'Dam'},
313                       rank_address=0,
314                       rank_search=16,
315                       centroid=(1.3, 0.70005))
316
317     api = frontend(apiobj, options=API_OPTIONS)
318     assert api.reverse((1.3, 0.7), layers=layer).place_id == place_id
319
320
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))')
323
324     api = frontend(apiobj, options=API_OPTIONS)
325     assert api.reverse((0.5, 0.5)) is None
326
327
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'},
335                       rank_address=4,
336                       rank_search=4,
337                       country_code='xx',
338                       centroid=(0.7, 0.7))
339
340     params = {'max_rank': rank}
341     if with_geom:
342         params['geometry_output'] = napi.GeometryFormat.TEXT
343
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
347
348
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',
354                       osm_type='N',
355                       name={'name': 'My State'},
356                       rank_address=6,
357                       rank_search=6,
358                       country_code='xx',
359                       centroid=(0.5, 0.505))
360     apiobj.add_placex(place_id=425, class_='place', type='state',
361                       osm_type='N',
362                       name={'name': 'Other State'},
363                       rank_address=6,
364                       rank_search=6,
365                       country_code='yy',
366                       centroid=(10.5, 0.505))
367
368     params = {'geometry_output': napi.GeometryFormat.KML} if with_geom else {}
369
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
373
374
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',
379                       housenumber='1',
380                       rank_address=30,
381                       rank_search=30,
382                       centroid=(59.3, 80.70001))
383     apiobj.add_placex(place_id=1003, class_='place', type='suburb',
384                       name={'name': 'Suburb Point'},
385                       osm_type='N',
386                       rank_address=18,
387                       rank_search=18,
388                       country_code='xx',
389                       centroid=(0.5, 0.5))
390
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
394
395
396 def test_reverse_simplified_geometry(apiobj, frontend):
397     apiobj.add_placex(place_id=1001, class_='place', type='house',
398                       housenumber='1',
399                       rank_address=30,
400                       rank_search=30,
401                       centroid=(59.3, 80.70001))
402
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
407
408
409 def test_reverse_interpolation_geometry(apiobj, frontend):
410     apiobj.add_osmline(place_id=992,
411                        parent_place_id=990,
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
416     api = frontend(apiobj, options=API_OPTIONS)
417     result = api.reverse((10.0, 10.0), geometry_output=napi.GeometryFormat.TEXT)
418
419     assert result.geometry['text'] == 'POINT(10 10.00001)'
420
421
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),
427                       country_code='us',
428                       geometry='LINESTRING(9.995 10, 10.005 10)')
429     apiobj.add_tiger(place_id=992,
430                      parent_place_id=990,
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),
438                       country_code='us',
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)')
445
446     api = frontend(apiobj, options=API_OPTIONS)
447
448     params = {'geometry_output': napi.GeometryFormat.GEOJSON}
449
450     output = api.reverse((10.0, 10.0), **params)
451     assert json.loads(output.geometry['geojson']) \
452         == {'coordinates': [10, 10.00001], 'type': 'Point'}
453
454     output = api.reverse((11.0, 11.0), **params)
455     assert json.loads(output.geometry['geojson']) \
456         == {'coordinates': [11, 11.00001], 'type': 'Point'}