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'}