1 # SPDX-License-Identifier: GPL-3.0-or-later
 
   3 # This file is part of Nominatim. (https://nominatim.org)
 
   5 # Copyright (C) 2024 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'}
 
  21 def test_reverse_rank_30(apiobj, frontend):
 
  22     apiobj.add_placex(place_id=223, class_='place', type='house',
 
  25                       geometry='POINT(1.3 0.7)')
 
  27     api = frontend(apiobj, options=API_OPTIONS)
 
  28     result = api.reverse((1.3, 0.7))
 
  30     assert result is not None
 
  31     assert result.place_id == 223
 
  34 @pytest.mark.parametrize('country', ['de', 'us'])
 
  35 def test_reverse_street(apiobj, frontend, country):
 
  36     apiobj.add_placex(place_id=990, class_='highway', type='service',
 
  37                       rank_search=27, rank_address=27,
 
  38                       name = {'name': 'My Street'},
 
  39                       centroid=(10.0, 10.0),
 
  41                       geometry='LINESTRING(9.995 10, 10.005 10)')
 
  43     api = frontend(apiobj, options=API_OPTIONS)
 
  44     assert api.reverse((9.995, 10)).place_id == 990
 
  47 def test_reverse_ignore_unindexed(apiobj, frontend):
 
  48     apiobj.add_placex(place_id=223, class_='place', type='house',
 
  52                       geometry='POINT(1.3 0.7)')
 
  54     api = frontend(apiobj, options=API_OPTIONS)
 
  55     result = api.reverse((1.3, 0.7))
 
  60 @pytest.mark.parametrize('y,layer,place_id', [(0.7, napi.DataLayer.ADDRESS, 223),
 
  61                                               (0.70001, napi.DataLayer.POI, 224),
 
  62                                               (0.7, napi.DataLayer.ADDRESS | napi.DataLayer.POI, 224),
 
  63                                               (0.70001, napi.DataLayer.ADDRESS | napi.DataLayer.POI, 223),
 
  64                                               (0.7, napi.DataLayer.MANMADE, 225),
 
  65                                               (0.7, napi.DataLayer.RAILWAY, 226),
 
  66                                               (0.7, napi.DataLayer.NATURAL, 227),
 
  67                                               (0.70003, napi.DataLayer.MANMADE | napi.DataLayer.RAILWAY, 225),
 
  68                                               (0.70003, napi.DataLayer.MANMADE | napi.DataLayer.NATURAL, 225),
 
  69                                               (5, napi.DataLayer.ADDRESS, 229)])
 
  70 def test_reverse_rank_30_layers(apiobj, frontend, y, layer, place_id):
 
  71     apiobj.add_placex(place_id=223, osm_type='N', class_='place', type='house',
 
  75                       centroid=(1.3, 0.70001))
 
  76     apiobj.add_placex(place_id=224, osm_type='N', class_='amenity', type='toilet',
 
  80     apiobj.add_placex(place_id=225, osm_type='N', class_='man_made', type='tower',
 
  83                       centroid=(1.3, 0.70003))
 
  84     apiobj.add_placex(place_id=226, osm_type='N', class_='railway', type='station',
 
  87                       centroid=(1.3, 0.70004))
 
  88     apiobj.add_placex(place_id=227, osm_type='N', class_='natural', type='cave',
 
  91                       centroid=(1.3, 0.70005))
 
  92     apiobj.add_placex(place_id=229, class_='place', type='house',
 
  93                       name={'addr:housename': 'Old Cottage'},
 
  98     api = frontend(apiobj, options=API_OPTIONS)
 
  99     assert api.reverse((1.3, y), layers=layer).place_id == place_id
 
 102 def test_reverse_poi_layer_with_no_pois(apiobj, frontend):
 
 103     apiobj.add_placex(place_id=223, class_='place', type='house',
 
 107                       centroid=(1.3, 0.70001))
 
 109     api = frontend(apiobj, options=API_OPTIONS)
 
 110     assert api.reverse((1.3, 0.70001), max_rank=29,
 
 111                               layers=napi.DataLayer.POI) is None
 
 114 @pytest.mark.parametrize('with_geom', [True, False])
 
 115 def test_reverse_housenumber_on_street(apiobj, frontend, with_geom):
 
 116     apiobj.add_placex(place_id=990, class_='highway', type='service',
 
 117                       rank_search=27, rank_address=27,
 
 118                       name = {'name': 'My Street'},
 
 119                       centroid=(10.0, 10.0),
 
 120                       geometry='LINESTRING(9.995 10, 10.005 10)')
 
 121     apiobj.add_placex(place_id=991, class_='place', type='house',
 
 123                       rank_search=30, rank_address=30,
 
 125                       centroid=(10.0, 10.00001))
 
 126     apiobj.add_placex(place_id=1990, class_='highway', type='service',
 
 127                       rank_search=27, rank_address=27,
 
 128                       name = {'name': 'Other Street'},
 
 129                       centroid=(10.0, 1.0),
 
 130                       geometry='LINESTRING(9.995 1, 10.005 1)')
 
 131     apiobj.add_placex(place_id=1991, class_='place', type='house',
 
 132                       parent_place_id=1990,
 
 133                       rank_search=30, rank_address=30,
 
 135                       centroid=(10.0, 1.00001))
 
 137     params = {'geometry_output': napi.GeometryFormat.TEXT} if with_geom else {}
 
 139     api = frontend(apiobj, options=API_OPTIONS)
 
 140     assert api.reverse((10.0, 10.0), max_rank=30, **params).place_id == 991
 
 141     assert api.reverse((10.0, 10.0), max_rank=27).place_id == 990
 
 142     assert api.reverse((10.0, 10.00001), max_rank=30).place_id == 991
 
 143     assert api.reverse((10.0, 1.0), **params).place_id == 1991
 
 146 @pytest.mark.parametrize('with_geom', [True, False])
 
 147 def test_reverse_housenumber_interpolation(apiobj, frontend, with_geom):
 
 148     apiobj.add_placex(place_id=990, class_='highway', type='service',
 
 149                       rank_search=27, rank_address=27,
 
 150                       name = {'name': 'My Street'},
 
 151                       centroid=(10.0, 10.0),
 
 152                       geometry='LINESTRING(9.995 10, 10.005 10)')
 
 153     apiobj.add_placex(place_id=991, class_='place', type='house',
 
 155                       rank_search=30, rank_address=30,
 
 157                       centroid=(10.0, 10.00002))
 
 158     apiobj.add_osmline(place_id=992,
 
 160                        startnumber=1, endnumber=3, step=1,
 
 161                        centroid=(10.0, 10.00001),
 
 162                        geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
 
 163     apiobj.add_placex(place_id=1990, class_='highway', type='service',
 
 164                       rank_search=27, rank_address=27,
 
 165                       name = {'name': 'Other Street'},
 
 166                       centroid=(10.0, 20.0),
 
 167                       geometry='LINESTRING(9.995 20, 10.005 20)')
 
 168     apiobj.add_osmline(place_id=1992,
 
 169                        parent_place_id=1990,
 
 170                        startnumber=1, endnumber=3, step=1,
 
 171                        centroid=(10.0, 20.00001),
 
 172                        geometry='LINESTRING(9.995 20.00001, 10.005 20.00001)')
 
 174     params = {'geometry_output': napi.GeometryFormat.TEXT} if with_geom else {}
 
 176     api = frontend(apiobj, options=API_OPTIONS)
 
 177     assert api.reverse((10.0, 10.0), **params).place_id == 992
 
 178     assert api.reverse((10.0, 20.0), **params).place_id == 1992
 
 181 def test_reverse_housenumber_point_interpolation(apiobj, frontend):
 
 182     apiobj.add_placex(place_id=990, class_='highway', type='service',
 
 183                       rank_search=27, rank_address=27,
 
 184                       name = {'name': 'My Street'},
 
 185                       centroid=(10.0, 10.0),
 
 186                       geometry='LINESTRING(9.995 10, 10.005 10)')
 
 187     apiobj.add_osmline(place_id=992,
 
 189                        startnumber=42, endnumber=42, step=1,
 
 190                        centroid=(10.0, 10.00001),
 
 191                        geometry='POINT(10.0 10.00001)')
 
 193     api = frontend(apiobj, options=API_OPTIONS)
 
 194     res = api.reverse((10.0, 10.0))
 
 195     assert res.place_id == 992
 
 196     assert res.housenumber == '42'
 
 199 def test_reverse_tiger_number(apiobj, frontend):
 
 200     apiobj.add_placex(place_id=990, class_='highway', type='service',
 
 201                       rank_search=27, rank_address=27,
 
 202                       name = {'name': 'My Street'},
 
 203                       centroid=(10.0, 10.0),
 
 205                       geometry='LINESTRING(9.995 10, 10.005 10)')
 
 206     apiobj.add_tiger(place_id=992,
 
 208                      startnumber=1, endnumber=3, step=1,
 
 209                      centroid=(10.0, 10.00001),
 
 210                      geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
 
 212     api = frontend(apiobj, options=API_OPTIONS)
 
 213     assert api.reverse((10.0, 10.0)).place_id == 992
 
 214     assert api.reverse((10.0, 10.00001)).place_id == 992
 
 217 def test_reverse_point_tiger(apiobj, frontend):
 
 218     apiobj.add_placex(place_id=990, class_='highway', type='service',
 
 219                       rank_search=27, rank_address=27,
 
 220                       name = {'name': 'My Street'},
 
 221                       centroid=(10.0, 10.0),
 
 223                       geometry='LINESTRING(9.995 10, 10.005 10)')
 
 224     apiobj.add_tiger(place_id=992,
 
 226                      startnumber=1, endnumber=1, step=1,
 
 227                      centroid=(10.0, 10.00001),
 
 228                      geometry='POINT(10.0 10.00001)')
 
 230     api = frontend(apiobj, options=API_OPTIONS)
 
 231     res = api.reverse((10.0, 10.0))
 
 232     assert res.place_id == 992
 
 233     assert res.housenumber == '1'
 
 236 def test_reverse_low_zoom_address(apiobj, frontend):
 
 237     apiobj.add_placex(place_id=1001, class_='place', type='house',
 
 241                       centroid=(59.3, 80.70001))
 
 242     apiobj.add_placex(place_id=1002, class_='place', type='town',
 
 243                       name={'name': 'Town'},
 
 246                       centroid=(59.3, 80.70001),
 
 247                       geometry="""POLYGON((59.3 80.70001, 59.3001 80.70001,
 
 248                                         59.3001 80.70101, 59.3 80.70101, 59.3 80.70001))""")
 
 250     api = frontend(apiobj, options=API_OPTIONS)
 
 251     assert api.reverse((59.30005, 80.7005)).place_id == 1001
 
 252     assert api.reverse((59.30005, 80.7005), max_rank=18).place_id == 1002
 
 255 def test_reverse_place_node_in_area(apiobj, frontend):
 
 256     apiobj.add_placex(place_id=1002, class_='place', type='town',
 
 257                       name={'name': 'Town Area'},
 
 260                       centroid=(59.3, 80.70001),
 
 261                       geometry="""POLYGON((59.3 80.70001, 59.3001 80.70001,
 
 262                                         59.3001 80.70101, 59.3 80.70101, 59.3 80.70001))""")
 
 263     apiobj.add_placex(place_id=1003, class_='place', type='suburb',
 
 264                       name={'name': 'Suburb Point'},
 
 268                       centroid=(59.30004, 80.70055))
 
 270     api = frontend(apiobj, options=API_OPTIONS)
 
 271     assert api.reverse((59.30004, 80.70055)).place_id == 1003
 
 274 @pytest.mark.parametrize('layer,place_id', [(napi.DataLayer.MANMADE, 225),
 
 275                                             (napi.DataLayer.RAILWAY, 226),
 
 276                                             (napi.DataLayer.NATURAL, 227),
 
 277                                             (napi.DataLayer.MANMADE | napi.DataLayer.RAILWAY, 225),
 
 278                                             (napi.DataLayer.MANMADE | napi.DataLayer.NATURAL, 225)])
 
 279 def test_reverse_larger_area_layers(apiobj, frontend, layer, place_id):
 
 280     apiobj.add_placex(place_id=225, class_='man_made', type='dam',
 
 281                       name={'name': 'Dam'},
 
 284                       centroid=(1.3, 0.70003))
 
 285     apiobj.add_placex(place_id=226, class_='railway', type='yard',
 
 286                       name={'name': 'Dam'},
 
 289                       centroid=(1.3, 0.70004))
 
 290     apiobj.add_placex(place_id=227, class_='natural', type='spring',
 
 291                       name={'name': 'Dam'},
 
 294                       centroid=(1.3, 0.70005))
 
 296     api = frontend(apiobj, options=API_OPTIONS)
 
 297     assert api.reverse((1.3, 0.7), layers=layer).place_id == place_id
 
 300 def test_reverse_country_lookup_no_objects(apiobj, frontend):
 
 301     apiobj.add_country('xx', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))')
 
 303     api = frontend(apiobj, options=API_OPTIONS)
 
 304     assert api.reverse((0.5, 0.5)) is None
 
 307 @pytest.mark.parametrize('rank', [4, 30])
 
 308 @pytest.mark.parametrize('with_geom', [True, False])
 
 309 def test_reverse_country_lookup_country_only(apiobj, frontend, rank, with_geom):
 
 310     apiobj.add_country('xx', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))')
 
 311     apiobj.add_country('yy', 'POLYGON((10 0, 10 1, 11 1, 11 0, 10 0))')
 
 312     apiobj.add_placex(place_id=225, class_='place', type='country',
 
 313                       name={'name': 'My Country'},
 
 319     params = {'max_rank': rank}
 
 321         params['geometry_output'] = napi.GeometryFormat.TEXT
 
 323     api = frontend(apiobj, options=API_OPTIONS)
 
 324     assert api.reverse((0.5, 0.5), **params).place_id == 225
 
 325     assert api.reverse((10.5, 0.5), **params) is None
 
 328 @pytest.mark.parametrize('with_geom', [True, False])
 
 329 def test_reverse_country_lookup_place_node_inside(apiobj, frontend, with_geom):
 
 330     apiobj.add_country('xx', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))')
 
 331     apiobj.add_country('yy', 'POLYGON((10 0, 10 1, 11 1, 11 0, 10 0))')
 
 332     apiobj.add_placex(place_id=225, class_='place', type='state',
 
 334                       name={'name': 'My State'},
 
 338                       centroid=(0.5, 0.505))
 
 339     apiobj.add_placex(place_id=425, class_='place', type='state',
 
 341                       name={'name': 'Other State'},
 
 345                       centroid=(10.5, 0.505))
 
 347     params = {'geometry_output': napi.GeometryFormat.KML} if with_geom else {}
 
 349     api = frontend(apiobj, options=API_OPTIONS)
 
 350     assert api.reverse((0.5, 0.5), **params).place_id == 225
 
 351     assert api.reverse((10.5, 0.5), **params).place_id == 425
 
 354 @pytest.mark.parametrize('gtype', list(napi.GeometryFormat))
 
 355 def test_reverse_geometry_output_placex(apiobj, frontend, gtype):
 
 356     apiobj.add_country('xx', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))')
 
 357     apiobj.add_placex(place_id=1001, class_='place', type='house',
 
 361                       centroid=(59.3, 80.70001))
 
 362     apiobj.add_placex(place_id=1003, class_='place', type='suburb',
 
 363                       name={'name': 'Suburb Point'},
 
 370     api = frontend(apiobj, options=API_OPTIONS)
 
 371     assert api.reverse((59.3, 80.70001), geometry_output=gtype).place_id == 1001
 
 372     assert api.reverse((0.5, 0.5), geometry_output=gtype).place_id == 1003
 
 375 def test_reverse_simplified_geometry(apiobj, frontend):
 
 376     apiobj.add_placex(place_id=1001, class_='place', type='house',
 
 380                       centroid=(59.3, 80.70001))
 
 382     api = frontend(apiobj, options=API_OPTIONS)
 
 383     details = dict(geometry_output=napi.GeometryFormat.GEOJSON,
 
 384                    geometry_simplification=0.1)
 
 385     assert api.reverse((59.3, 80.70001), **details).place_id == 1001
 
 388 def test_reverse_interpolation_geometry(apiobj, frontend):
 
 389     apiobj.add_osmline(place_id=992,
 
 391                        startnumber=1, endnumber=3, step=1,
 
 392                        centroid=(10.0, 10.00001),
 
 393                        geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
 
 395     api = frontend(apiobj, options=API_OPTIONS)
 
 396     assert api.reverse((10.0, 10.0), geometry_output=napi.GeometryFormat.TEXT)\
 
 397                      .geometry['text'] == 'POINT(10 10.00001)'
 
 400 def test_reverse_tiger_geometry(apiobj, frontend):
 
 401     apiobj.add_placex(place_id=990, class_='highway', type='service',
 
 402                       rank_search=27, rank_address=27,
 
 403                       name = {'name': 'My Street'},
 
 404                       centroid=(10.0, 10.0),
 
 406                       geometry='LINESTRING(9.995 10, 10.005 10)')
 
 407     apiobj.add_tiger(place_id=992,
 
 409                      startnumber=1, endnumber=3, step=1,
 
 410                      centroid=(10.0, 10.00001),
 
 411                      geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)')
 
 412     apiobj.add_placex(place_id=1000, class_='highway', type='service',
 
 413                       rank_search=27, rank_address=27,
 
 414                       name = {'name': 'My Street'},
 
 415                       centroid=(11.0, 11.0),
 
 417                       geometry='LINESTRING(10.995 11, 11.005 11)')
 
 418     apiobj.add_tiger(place_id=1001,
 
 419                      parent_place_id=1000,
 
 420                      startnumber=1, endnumber=3, step=1,
 
 421                      centroid=(11.0, 11.00001),
 
 422                      geometry='LINESTRING(10.995 11.00001, 11.005 11.00001)')
 
 424     api = frontend(apiobj, options=API_OPTIONS)
 
 426     params = {'geometry_output': napi.GeometryFormat.GEOJSON}
 
 428     output = api.reverse((10.0, 10.0), **params)
 
 429     assert json.loads(output.geometry['geojson']) == {'coordinates': [10, 10.00001], 'type': 'Point'}
 
 431     output = api.reverse((11.0, 11.0), **params)
 
 432     assert json.loads(output.geometry['geojson']) == {'coordinates': [11, 11.00001], 'type': 'Point'}