From b00b16aa3a975ac386ba7c4ddeae3090669af274 Mon Sep 17 00:00:00 2001 From: Sarah Hoffmann Date: Tue, 26 Sep 2023 09:40:30 +0200 Subject: [PATCH] more unit tests for search --- nominatim/api/results.py | 3 +- nominatim/api/search/db_searches.py | 9 +- test/python/api/search/test_search_country.py | 67 +++++++++++++ test/python/api/search/test_search_near.py | 67 ++++++++++++- test/python/api/search/test_search_places.py | 99 +++++++++++++++++-- .../python/api/search/test_search_postcode.py | 67 ++++++++++++- 6 files changed, 298 insertions(+), 14 deletions(-) diff --git a/nominatim/api/results.py b/nominatim/api/results.py index b504fefa..ae1ae4ac 100644 --- a/nominatim/api/results.py +++ b/nominatim/api/results.py @@ -435,7 +435,8 @@ def create_from_country_row(row: Optional[SaRow], centroid=Point.from_wkb(row.centroid), names=row.name, rank_address=4, rank_search=4, - country_code=row.country_code) + country_code=row.country_code, + geometry=_filter_geometries(row)) async def add_result_details(conn: SearchConnection, results: List[BaseResultT], diff --git a/nominatim/api/search/db_searches.py b/nominatim/api/search/db_searches.py index 047b6220..eb4da549 100644 --- a/nominatim/api/search/db_searches.py +++ b/nominatim/api/search/db_searches.py @@ -449,7 +449,8 @@ class CountrySearch(AbstractSearch): sql = sa.select(tgrid.c.country_code, tgrid.c.geometry.ST_Centroid().ST_Collect().ST_Centroid() - .label('centroid'))\ + .label('centroid'), + tgrid.c.geometry.ST_Collect().ST_Expand(0).label('bbox'))\ .where(tgrid.c.country_code.in_(self.countries.values))\ .group_by(tgrid.c.country_code) @@ -465,13 +466,17 @@ class CountrySearch(AbstractSearch): + sa.func.coalesce(t.c.derived_name, sa.cast('', type_=conn.t.types.Composite)) ).label('name'), - sub.c.centroid)\ + sub.c.centroid, sub.c.bbox)\ .join(sub, t.c.country_code == sub.c.country_code) + if details.geometry_output: + sql = _add_geometry_columns(sql, sub.c.centroid, details) + results = nres.SearchResults() for row in await conn.execute(sql, _details_to_bind_params(details)): result = nres.create_from_country_row(row, nres.SearchResult) assert result + result.bbox = Bbox.from_wkb(row.bbox) result.accuracy = self.penalty + self.countries.get_penalty(row.country_code, 5.0) results.append(result) diff --git a/test/python/api/search/test_search_country.py b/test/python/api/search/test_search_country.py index bb0abc39..82b1d37f 100644 --- a/test/python/api/search/test_search_country.py +++ b/test/python/api/search/test_search_country.py @@ -59,3 +59,70 @@ def test_find_from_fallback_countries(apiobj): def test_find_none(apiobj): assert len(run_search(apiobj, 0.0, ['xx'])) == 0 + + +@pytest.mark.parametrize('coord,numres', [((0.5, 1), 1), ((10, 10), 0)]) +def test_find_near(apiobj, coord, numres): + apiobj.add_country('ro', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))') + apiobj.add_country_name('ro', {'name': 'România'}) + + results = run_search(apiobj, 0.0, ['ro'], + details=SearchDetails(near=napi.Point(*coord), + near_radius=0.1)) + + assert len(results) == numres + + +class TestCountryParameters: + + @pytest.fixture(autouse=True) + def fill_database(self, apiobj): + apiobj.add_placex(place_id=55, class_='boundary', type='administrative', + rank_search=4, rank_address=4, + name={'name': 'Lolaland'}, + country_code='yw', + centroid=(10, 10), + geometry='POLYGON((9.5 9.5, 9.5 10.5, 10.5 10.5, 10.5 9.5, 9.5 9.5))') + apiobj.add_country('ro', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))') + apiobj.add_country_name('ro', {'name': 'România'}) + + + @pytest.mark.parametrize('geom', [napi.GeometryFormat.GEOJSON, + napi.GeometryFormat.KML, + napi.GeometryFormat.SVG, + napi.GeometryFormat.TEXT]) + @pytest.mark.parametrize('cc', ['yw', 'ro']) + def test_return_geometries(self, apiobj, geom, cc): + results = run_search(apiobj, 0.5, [cc], + details=SearchDetails(geometry_output=geom)) + + assert len(results) == 1 + assert geom.name.lower() in results[0].geometry + + + @pytest.mark.parametrize('pid,rids', [(76, [55]), (55, [])]) + def test_exclude_place_id(self, apiobj, pid, rids): + results = run_search(apiobj, 0.5, ['yw', 'ro'], + details=SearchDetails(excluded=[pid])) + + assert [r.place_id for r in results] == rids + + + @pytest.mark.parametrize('viewbox,rids', [((9, 9, 11, 11), [55]), + ((-10, -10, -3, -3), [])]) + def test_bounded_viewbox_in_placex(self, apiobj, viewbox, rids): + results = run_search(apiobj, 0.5, ['yw'], + details=SearchDetails.from_kwargs({'viewbox': viewbox, + 'bounded_viewbox': True})) + + assert [r.place_id for r in results] == rids + + + @pytest.mark.parametrize('viewbox,numres', [((0, 0, 1, 1), 1), + ((-10, -10, -3, -3), 0)]) + def test_bounded_viewbox_in_fallback(self, apiobj, viewbox, numres): + results = run_search(apiobj, 0.5, ['ro'], + details=SearchDetails.from_kwargs({'viewbox': viewbox, + 'bounded_viewbox': True})) + + assert len(results) == numres diff --git a/test/python/api/search/test_search_near.py b/test/python/api/search/test_search_near.py index cfbdadb2..2a0acb74 100644 --- a/test/python/api/search/test_search_near.py +++ b/test/python/api/search/test_search_near.py @@ -16,18 +16,21 @@ from nominatim.api.search.db_search_fields import WeightedStrings, WeightedCateg FieldLookup, FieldRanking, RankedTokens -def run_search(apiobj, global_penalty, cat, cat_penalty=None, +def run_search(apiobj, global_penalty, cat, cat_penalty=None, ccodes=[], details=SearchDetails()): class PlaceSearchData: penalty = 0.0 postcodes = WeightedStrings([], []) - countries = WeightedStrings([], []) + countries = WeightedStrings(ccodes, [0.0] * len(ccodes)) housenumbers = WeightedStrings([], []) qualifiers = WeightedStrings([], []) lookups = [FieldLookup('name_vector', [56], 'lookup_all')] rankings = [] + if ccodes is not None: + details.countries = ccodes + place_search = PlaceSearch(0.0, PlaceSearchData(), 2) if cat_penalty is None: @@ -49,6 +52,18 @@ def test_no_results_inner_query(apiobj): assert not run_search(apiobj, 0.4, [('this', 'that')]) +def test_no_appropriate_results_inner_query(apiobj): + apiobj.add_placex(place_id=100, country_code='us', + centroid=(5.6, 4.3), + geometry='POLYGON((0.0 0.0, 10.0 0.0, 10.0 2.0, 0.0 2.0, 0.0 0.0))') + apiobj.add_search_name(100, names=[56], country_code='us', + centroid=(5.6, 4.3)) + apiobj.add_placex(place_id=22, class_='amenity', type='bank', + centroid=(5.6001, 4.2994)) + + assert not run_search(apiobj, 0.4, [('amenity', 'bank')]) + + class TestNearSearch: @pytest.fixture(autouse=True) @@ -100,3 +115,51 @@ class TestNearSearch: assert [r.place_id for r in results] == [22] + + @pytest.mark.parametrize('cc,rid', [('us', 22), ('mx', 23)]) + def test_restrict_by_country(self, apiobj, cc, rid): + apiobj.add_placex(place_id=22, class_='amenity', type='bank', + centroid=(5.6001, 4.2994), + country_code='us') + apiobj.add_placex(place_id=122, class_='amenity', type='bank', + centroid=(5.6001, 4.2994), + country_code='mx') + apiobj.add_placex(place_id=23, class_='amenity', type='bank', + centroid=(-10.3001, 56.9), + country_code='mx') + apiobj.add_placex(place_id=123, class_='amenity', type='bank', + centroid=(-10.3001, 56.9), + country_code='us') + + results = run_search(apiobj, 0.1, [('amenity', 'bank')], ccodes=[cc, 'fr']) + + assert [r.place_id for r in results] == [rid] + + + @pytest.mark.parametrize('excluded,rid', [(22, 122), (122, 22)]) + def test_exclude_place_by_id(self, apiobj, excluded, rid): + apiobj.add_placex(place_id=22, class_='amenity', type='bank', + centroid=(5.6001, 4.2994), + country_code='us') + apiobj.add_placex(place_id=122, class_='amenity', type='bank', + centroid=(5.6001, 4.2994), + country_code='us') + + + results = run_search(apiobj, 0.1, [('amenity', 'bank')], + details=SearchDetails(excluded=[excluded])) + + assert [r.place_id for r in results] == [rid] + + + @pytest.mark.parametrize('layer,rids', [(napi.DataLayer.POI, [22]), + (napi.DataLayer.MANMADE, [])]) + def test_with_layer(self, apiobj, layer, rids): + apiobj.add_placex(place_id=22, class_='amenity', type='bank', + centroid=(5.6001, 4.2994), + country_code='us') + + results = run_search(apiobj, 0.1, [('amenity', 'bank')], + details=SearchDetails(layers=layer)) + + assert [r.place_id for r in results] == rids diff --git a/test/python/api/search/test_search_places.py b/test/python/api/search/test_search_places.py index 8d17ec2d..d280eeee 100644 --- a/test/python/api/search/test_search_places.py +++ b/test/python/api/search/test_search_places.py @@ -7,6 +7,8 @@ """ Tests for running the generic place searcher. """ +import json + import pytest import nominatim.api as napi @@ -130,23 +132,48 @@ class TestNameOnlySearches: assert geom.name.lower() in results[0].geometry + @pytest.mark.parametrize('factor,npoints', [(0.0, 3), (1.0, 2)]) + def test_return_simplified_geometry(self, apiobj, factor, npoints): + apiobj.add_placex(place_id=333, country_code='us', + centroid=(9.0, 9.0), + geometry='LINESTRING(8.9 9.0, 9.0 9.0, 9.1 9.0)') + apiobj.add_search_name(333, names=[55], country_code='us', + centroid=(5.6, 4.3)) + + lookup = FieldLookup('name_vector', [55], 'lookup_all') + ranking = FieldRanking('name_vector', 0.9, [RankedTokens(0.0, [21])]) + + results = run_search(apiobj, 0.1, [lookup], [ranking], + details=SearchDetails(geometry_output=napi.GeometryFormat.GEOJSON, + geometry_simplification=factor)) + + assert len(results) == 1 + result = results[0] + geom = json.loads(result.geometry['geojson']) + + assert result.place_id == 333 + assert len(geom['coordinates']) == npoints + + @pytest.mark.parametrize('viewbox', ['5.0,4.0,6.0,5.0', '5.7,4.0,6.0,5.0']) - def test_prefer_viewbox(self, apiobj, viewbox): + @pytest.mark.parametrize('wcount,rids', [(2, [100, 101]), (20000, [100])]) + def test_prefer_viewbox(self, apiobj, viewbox, wcount, rids): lookup = FieldLookup('name_vector', [1, 2], 'lookup_all') ranking = FieldRanking('name_vector', 0.9, [RankedTokens(0.0, [21])]) results = run_search(apiobj, 0.1, [lookup], [ranking]) assert [r.place_id for r in results] == [101, 100] - results = run_search(apiobj, 0.1, [lookup], [ranking], + results = run_search(apiobj, 0.1, [lookup], [ranking], count=wcount, details=SearchDetails.from_kwargs({'viewbox': viewbox})) - assert [r.place_id for r in results] == [100, 101] + assert [r.place_id for r in results] == rids - def test_force_viewbox(self, apiobj): + @pytest.mark.parametrize('viewbox', ['5.0,4.0,6.0,5.0', '5.55,4.27,5.62,4.31']) + def test_force_viewbox(self, apiobj, viewbox): lookup = FieldLookup('name_vector', [1, 2], 'lookup_all') - details=SearchDetails.from_kwargs({'viewbox': '5.0,4.0,6.0,5.0', + details=SearchDetails.from_kwargs({'viewbox': viewbox, 'bounded_viewbox': True}) results = run_search(apiobj, 0.1, [lookup], [], details=details) @@ -166,11 +193,12 @@ class TestNameOnlySearches: assert [r.place_id for r in results] == [100, 101] - def test_force_near(self, apiobj): + @pytest.mark.parametrize('radius', [0.09, 0.11]) + def test_force_near(self, apiobj, radius): lookup = FieldLookup('name_vector', [1, 2], 'lookup_all') details=SearchDetails.from_kwargs({'near': '5.6,4.3', - 'near_radius': 0.11}) + 'near_radius': radius}) results = run_search(apiobj, 0.1, [lookup], [], details=details) @@ -287,6 +315,34 @@ def test_very_large_housenumber(apiobj): assert [r.place_id for r in results] == [93, 2000] +@pytest.mark.parametrize('wcount,rids', [(2, [990, 991]), (30000, [990])]) +def test_name_and_postcode(apiobj, wcount, rids): + apiobj.add_placex(place_id=990, class_='highway', type='service', + rank_search=27, rank_address=27, + postcode='11225', + centroid=(10.0, 10.0), + geometry='LINESTRING(9.995 10, 10.005 10)') + apiobj.add_search_name(990, names=[111], centroid=(10.0, 10.0), + search_rank=27, address_rank=27) + apiobj.add_placex(place_id=991, class_='highway', type='service', + rank_search=27, rank_address=27, + postcode='11221', + centroid=(10.1, 10.1), + geometry='LINESTRING(9.995 10.1, 10.005 10.1)') + apiobj.add_search_name(991, names=[111], centroid=(10.1, 10.1), + search_rank=27, address_rank=27) + apiobj.add_postcode(place_id=100, country_code='ch', postcode='11225', + geometry='POINT(10 10)') + + lookup = FieldLookup('name_vector', [111], 'lookup_all') + + results = run_search(apiobj, 0.1, [lookup], [], pcs=['11225'], count=wcount, + details=SearchDetails()) + + assert results + assert [r.place_id for r in results] == rids + + class TestInterpolations: @pytest.fixture(autouse=True) @@ -318,6 +374,21 @@ class TestInterpolations: assert [r.place_id for r in results] == res + [990] + @pytest.mark.parametrize('geom', [napi.GeometryFormat.GEOJSON, + napi.GeometryFormat.KML, + napi.GeometryFormat.SVG, + napi.GeometryFormat.TEXT]) + def test_osmline_with_geometries(self, apiobj, geom): + lookup = FieldLookup('name_vector', [111], 'lookup_all') + + results = run_search(apiobj, 0.1, [lookup], [], hnrs=['21'], + details=SearchDetails(geometry_output=geom)) + + assert results[0].place_id == 992 + assert geom.name.lower() in results[0].geometry + + + class TestTiger: @pytest.fixture(autouse=True) @@ -351,6 +422,20 @@ class TestTiger: assert [r.place_id for r in results] == res + [990] + @pytest.mark.parametrize('geom', [napi.GeometryFormat.GEOJSON, + napi.GeometryFormat.KML, + napi.GeometryFormat.SVG, + napi.GeometryFormat.TEXT]) + def test_tiger_with_geometries(self, apiobj, geom): + lookup = FieldLookup('name_vector', [111], 'lookup_all') + + results = run_search(apiobj, 0.1, [lookup], [], hnrs=['21'], + details=SearchDetails(geometry_output=geom)) + + assert results[0].place_id == 992 + assert geom.name.lower() in results[0].geometry + + class TestLayersRank30: @pytest.fixture(autouse=True) diff --git a/test/python/api/search/test_search_postcode.py b/test/python/api/search/test_search_postcode.py index a43bc897..e7153f38 100644 --- a/test/python/api/search/test_search_postcode.py +++ b/test/python/api/search/test_search_postcode.py @@ -62,9 +62,11 @@ class TestPostcodeSearchWithAddress: @pytest.fixture(autouse=True) def fill_database(self, apiobj): apiobj.add_postcode(place_id=100, country_code='ch', - parent_place_id=1000, postcode='12345') + parent_place_id=1000, postcode='12345', + geometry='POINT(17 5)') apiobj.add_postcode(place_id=101, country_code='pl', - parent_place_id=2000, postcode='12345') + parent_place_id=2000, postcode='12345', + geometry='POINT(-45 7)') apiobj.add_placex(place_id=1000, class_='place', type='village', rank_search=22, rank_address=22, country_code='ch') @@ -95,3 +97,64 @@ class TestPostcodeSearchWithAddress: assert [r.place_id for r in results] == [100] + + @pytest.mark.parametrize('coord,place_id', [((16.5, 5), 100), + ((-45.1, 7.004), 101)]) + def test_lookup_near(self, apiobj, coord, place_id): + lookup = FieldLookup('name_vector', [1,2], 'restrict') + ranking = FieldRanking('name_vector', 0.3, [RankedTokens(0.0, [10])]) + + results = run_search(apiobj, 0.1, ['12345'], + lookup=[lookup], ranking=[ranking], + details=SearchDetails(near=napi.Point(*coord), + near_radius=0.6)) + + assert [r.place_id for r in results] == [place_id] + + + @pytest.mark.parametrize('geom', [napi.GeometryFormat.GEOJSON, + napi.GeometryFormat.KML, + napi.GeometryFormat.SVG, + napi.GeometryFormat.TEXT]) + def test_return_geometries(self, apiobj, geom): + results = run_search(apiobj, 0.1, ['12345'], + details=SearchDetails(geometry_output=geom)) + + assert results + assert all(geom.name.lower() in r.geometry for r in results) + + + @pytest.mark.parametrize('viewbox, rids', [('-46,6,-44,8', [101,100]), + ('16,4,18,6', [100,101])]) + def test_prefer_viewbox(self, apiobj, viewbox, rids): + results = run_search(apiobj, 0.1, ['12345'], + details=SearchDetails.from_kwargs({'viewbox': viewbox})) + + assert [r.place_id for r in results] == rids + + + @pytest.mark.parametrize('viewbox, rid', [('-46,6,-44,8', 101), + ('16,4,18,6', 100)]) + def test_restrict_to_viewbox(self, apiobj, viewbox, rid): + results = run_search(apiobj, 0.1, ['12345'], + details=SearchDetails.from_kwargs({'viewbox': viewbox, + 'bounded_viewbox': True})) + + assert [r.place_id for r in results] == [rid] + + + @pytest.mark.parametrize('coord,rids', [((17.05, 5), [100, 101]), + ((-45, 7.1), [101, 100])]) + def test_prefer_near(self, apiobj, coord, rids): + results = run_search(apiobj, 0.1, ['12345'], + details=SearchDetails(near=napi.Point(*coord))) + + assert [r.place_id for r in results] == rids + + + @pytest.mark.parametrize('pid,rid', [(100, 101), (101, 100)]) + def test_exclude(self, apiobj, pid, rid): + results = run_search(apiobj, 0.1, ['12345'], + details=SearchDetails(excluded=[pid])) + + assert [r.place_id for r in results] == [rid] -- 2.39.5