From 041794615320f836ed58511437d29ba0b1379777 Mon Sep 17 00:00:00 2001 From: Sarah Hoffmann Date: Fri, 13 Oct 2023 11:03:13 +0200 Subject: [PATCH] make reverse API work with sqlite --- nominatim/api/reverse.py | 24 ++--- nominatim/db/sqlalchemy_functions.py | 129 ++++++++++++++++++++++++--- nominatim/db/sqlalchemy_schema.py | 1 + nominatim/db/sqlalchemy_types.py | 95 ++++++++++++++++++-- test/python/api/test_api_reverse.py | 112 +++++++++++++---------- 5 files changed, 282 insertions(+), 79 deletions(-) diff --git a/nominatim/api/reverse.py b/nominatim/api/reverse.py index d46733f0..e0c7e460 100644 --- a/nominatim/api/reverse.py +++ b/nominatim/api/reverse.py @@ -19,7 +19,6 @@ import nominatim.api.results as nres from nominatim.api.logging import log from nominatim.api.types import AnyPoint, DataLayer, ReverseDetails, GeometryFormat, Bbox from nominatim.db.sqlalchemy_types import Geometry -import nominatim.db.sqlalchemy_functions as snfn # In SQLAlchemy expression which compare with NULL need to be expressed with # the equal sign. @@ -88,7 +87,7 @@ def _locate_interpolation(table: SaFromClause) -> SaLabel: def _is_address_point(table: SaFromClause) -> SaColumn: return sa.and_(table.c.rank_address == 30, sa.or_(table.c.housenumber != None, - table.c.name.has_key('addr:housename'))) + sa.func.JsonHasKey(table.c.name, 'addr:housename'))) def _get_closest(*rows: Optional[SaRow]) -> Optional[SaRow]: @@ -371,7 +370,7 @@ class ReverseGeocoder: inner = sa.select(t, sa.literal(0.0).label('distance'))\ .where(t.c.rank_search.between(5, MAX_RANK_PARAM))\ .where(t.c.geometry.intersects(WKT_PARAM))\ - .where(snfn.select_index_placex_geometry_reverse_lookuppolygon('placex'))\ + .where(sa.func.PlacexGeometryReverseLookuppolygon())\ .order_by(sa.desc(t.c.rank_search))\ .limit(50)\ .subquery('area') @@ -401,10 +400,7 @@ class ReverseGeocoder: .where(t.c.rank_search > address_rank)\ .where(t.c.rank_search <= MAX_RANK_PARAM)\ .where(t.c.indexed_status == 0)\ - .where(snfn.select_index_placex_geometry_reverse_lookupplacenode('placex'))\ - .where(t.c.geometry - .ST_Buffer(sa.func.reverse_place_diameter(t.c.rank_search)) - .intersects(WKT_PARAM))\ + .where(sa.func.IntersectsReverseDistance(t, WKT_PARAM))\ .order_by(sa.desc(t.c.rank_search))\ .limit(50)\ .subquery('places') @@ -413,7 +409,7 @@ class ReverseGeocoder: return _select_from_placex(inner, False)\ .join(touter, touter.c.geometry.ST_Contains(inner.c.geometry))\ .where(touter.c.place_id == address_id)\ - .where(inner.c.distance < sa.func.reverse_place_diameter(inner.c.rank_search))\ + .where(sa.func.IsBelowReverseDistance(inner.c.distance, inner.c.rank_search))\ .order_by(sa.desc(inner.c.rank_search), inner.c.distance)\ .limit(1) @@ -440,10 +436,9 @@ class ReverseGeocoder: .where(t.c.indexed_status == 0)\ .where(t.c.linked_place_id == None)\ .where(self._filter_by_layer(t))\ - .where(t.c.geometry - .ST_Buffer(sa.func.reverse_place_diameter(t.c.rank_search)) - .intersects(WKT_PARAM))\ + .where(t.c.geometry.intersects(sa.func.ST_Expand(WKT_PARAM, 0.001)))\ .order_by(sa.desc(t.c.rank_search))\ + .order_by('distance')\ .limit(50)\ .subquery() @@ -514,16 +509,13 @@ class ReverseGeocoder: .where(t.c.rank_search <= MAX_RANK_PARAM)\ .where(t.c.indexed_status == 0)\ .where(t.c.country_code.in_(ccodes))\ - .where(snfn.select_index_placex_geometry_reverse_lookupplacenode('placex'))\ - .where(t.c.geometry - .ST_Buffer(sa.func.reverse_place_diameter(t.c.rank_search)) - .intersects(WKT_PARAM))\ + .where(sa.func.IntersectsReverseDistance(t, WKT_PARAM))\ .order_by(sa.desc(t.c.rank_search))\ .limit(50)\ .subquery('area') return _select_from_placex(inner, False)\ - .where(inner.c.distance < sa.func.reverse_place_diameter(inner.c.rank_search))\ + .where(sa.func.IsBelowReverseDistance(inner.c.distance, inner.c.rank_search))\ .order_by(sa.desc(inner.c.rank_search), inner.c.distance)\ .limit(1) diff --git a/nominatim/db/sqlalchemy_functions.py b/nominatim/db/sqlalchemy_functions.py index 6eed8803..6a5809bd 100644 --- a/nominatim/db/sqlalchemy_functions.py +++ b/nominatim/db/sqlalchemy_functions.py @@ -15,19 +15,110 @@ from sqlalchemy.ext.compiler import compiles from nominatim.typing import SaColumn -# pylint: disable=abstract-method,missing-function-docstring,consider-using-f-string +# pylint: disable=all -def select_index_placex_geometry_reverse_lookuppolygon(table: str) -> 'sa.TextClause': - """ Create an expression with the necessary conditions over a placex - table that the index 'idx_placex_geometry_reverse_lookupPolygon' - can be used. +class PlacexGeometryReverseLookuppolygon(sa.sql.functions.GenericFunction[bool]): + """ Check for conditions that allow partial index use on + 'idx_placex_geometry_reverse_lookupPolygon'. + + Needs to be constant, so that the query planner picks them up correctly + in prepared statements. """ - return sa.text(f"ST_GeometryType({table}.geometry) in ('ST_Polygon', 'ST_MultiPolygon')" - f" AND {table}.rank_address between 4 and 25" - f" AND {table}.type != 'postcode'" - f" AND {table}.name is not null" - f" AND {table}.indexed_status = 0" - f" AND {table}.linked_place_id is null") + type = sa.Boolean() + name = 'PlacexGeometryReverseLookuppolygon' + inherit_cache = True + + +@compiles(PlacexGeometryReverseLookuppolygon) # type: ignore[no-untyped-call, misc] +def _default_intersects(element: SaColumn, + compiler: 'sa.Compiled', **kw: Any) -> str: + return ("(ST_GeometryType(placex.geometry) in ('ST_Polygon', 'ST_MultiPolygon')" + " AND placex.rank_address between 4 and 25" + " AND placex.type != 'postcode'" + " AND placex.name is not null" + " AND placex.indexed_status = 0" + " AND placex.linked_place_id is null)") + + +@compiles(PlacexGeometryReverseLookuppolygon, 'sqlite') # type: ignore[no-untyped-call, misc] +def _sqlite_intersects(element: SaColumn, + compiler: 'sa.Compiled', **kw: Any) -> str: + return ("(ST_GeometryType(placex.geometry) in ('POLYGON', 'MULTIPOLYGON')" + " AND placex.rank_address between 4 and 25" + " AND placex.type != 'postcode'" + " AND placex.name is not null" + " AND placex.indexed_status = 0" + " AND placex.linked_place_id is null)") + + +class IntersectsReverseDistance(sa.sql.functions.GenericFunction[bool]): + type = sa.Boolean() + name = 'IntersectsReverseDistance' + inherit_cache = True + + def __init__(self, table: sa.Table, geom: SaColumn) -> None: + super().__init__(table.c.geometry, # type: ignore[no-untyped-call] + table.c.rank_search, geom) + self.tablename = table.name + + +@compiles(IntersectsReverseDistance) # type: ignore[no-untyped-call, misc] +def default_reverse_place_diameter(element: SaColumn, + compiler: 'sa.Compiled', **kw: Any) -> str: + table = element.tablename + return f"{table}.rank_address between 4 and 25"\ + f" AND {table}.type != 'postcode'"\ + f" AND {table}.name is not null"\ + f" AND {table}.linked_place_id is null"\ + f" AND {table}.osm_type = 'N'" + \ + " AND ST_Buffer(%s, reverse_place_diameter(%s)) && %s" % \ + tuple(map(lambda c: compiler.process(c, **kw), element.clauses)) + + +@compiles(IntersectsReverseDistance, 'sqlite') # type: ignore[no-untyped-call, misc] +def sqlite_reverse_place_diameter(element: SaColumn, + compiler: 'sa.Compiled', **kw: Any) -> str: + geom1, rank, geom2 = list(element.clauses) + table = element.tablename + + return (f"{table}.rank_address between 4 and 25"\ + f" AND {table}.type != 'postcode'"\ + f" AND {table}.name is not null"\ + f" AND {table}.linked_place_id is null"\ + f" AND {table}.osm_type = 'N'"\ + " AND MbrIntersects(%s, ST_Expand(%s, 14.0 * exp(-0.2 * %s) - 0.03))"\ + f" AND {table}.place_id IN"\ + " (SELECT place_id FROM placex_place_node_areas"\ + " WHERE ROWID IN (SELECT ROWID FROM SpatialIndex"\ + " WHERE f_table_name = 'placex_place_node_areas'"\ + " AND search_frame = %s))") % ( + compiler.process(geom1, **kw), + compiler.process(geom2, **kw), + compiler.process(rank, **kw), + compiler.process(geom2, **kw)) + + +class IsBelowReverseDistance(sa.sql.functions.GenericFunction[bool]): + type = sa.Boolean() + name = 'IsBelowReverseDistance' + inherit_cache = True + + +@compiles(IsBelowReverseDistance) # type: ignore[no-untyped-call, misc] +def default_is_below_reverse_distance(element: SaColumn, + compiler: 'sa.Compiled', **kw: Any) -> str: + dist, rank = list(element.clauses) + return "%s < reverse_place_diameter(%s)" % (compiler.process(dist, **kw), + compiler.process(rank, **kw)) + + +@compiles(IsBelowReverseDistance, 'sqlite') # type: ignore[no-untyped-call, misc] +def sqlite_is_below_reverse_distance(element: SaColumn, + compiler: 'sa.Compiled', **kw: Any) -> str: + dist, rank = list(element.clauses) + return "%s < 14.0 * exp(-0.2 * %s) - 0.03" % (compiler.process(dist, **kw), + compiler.process(rank, **kw)) + def select_index_placex_geometry_reverse_lookupplacenode(table: str) -> 'sa.TextClause': """ Create an expression with the necessary conditions over a placex @@ -84,6 +175,22 @@ def sqlite_json_array_each(element: SaColumn, compiler: 'sa.Compiled', **kw: Any return "json_each(%s)" % compiler.process(element.clauses, **kw) +class JsonHasKey(sa.sql.functions.GenericFunction[bool]): + """ Return elements of a json array as a set. + """ + type = sa.Boolean() + name = 'JsonHasKey' + inherit_cache = True + + +@compiles(JsonHasKey) # type: ignore[no-untyped-call, misc] +def compile_json_has_key(element: SaColumn, + compiler: 'sa.Compiled', **kw: Any) -> str: + arg1, arg2 = list(element.clauses) + return "%s->%s is not null" % (compiler.process(arg1, **kw), + compiler.process(arg2, **kw)) + + class Greatest(sa.sql.functions.GenericFunction[Any]): """ Function to compute maximum of all its input parameters. """ diff --git a/nominatim/db/sqlalchemy_schema.py b/nominatim/db/sqlalchemy_schema.py index 3b769eb3..7dd1e0ce 100644 --- a/nominatim/db/sqlalchemy_schema.py +++ b/nominatim/db/sqlalchemy_schema.py @@ -13,6 +13,7 @@ import sqlalchemy as sa from sqlalchemy.dialects.postgresql import HSTORE, ARRAY, JSONB, array from sqlalchemy.dialects.sqlite import JSON as sqlite_json +import nominatim.db.sqlalchemy_functions #pylint: disable=unused-import from nominatim.db.sqlalchemy_types import Geometry class PostgresTypes: diff --git a/nominatim/db/sqlalchemy_types.py b/nominatim/db/sqlalchemy_types.py index 4171d70b..df2bf150 100644 --- a/nominatim/db/sqlalchemy_types.py +++ b/nominatim/db/sqlalchemy_types.py @@ -41,6 +41,71 @@ def _spatialite_distance_spheroid(element: SaColumn, return "Distance(%s, true)" % compiler.process(element.clauses, **kw) +class Geometry_IsLineLike(sa.sql.expression.FunctionElement[bool]): + """ Check if the geometry is a line or multiline. + """ + type = sa.Boolean() + name = 'Geometry_IsLineLike' + inherit_cache = True + + +@compiles(Geometry_IsLineLike) # type: ignore[no-untyped-call, misc] +def _default_is_line_like(element: SaColumn, + compiler: 'sa.Compiled', **kw: Any) -> str: + return "ST_GeometryType(%s) IN ('ST_LineString', 'ST_MultiLineString')" % \ + compiler.process(element.clauses, **kw) + + +@compiles(Geometry_IsLineLike, 'sqlite') # type: ignore[no-untyped-call, misc] +def _sqlite_is_line_like(element: SaColumn, + compiler: 'sa.Compiled', **kw: Any) -> str: + return "ST_GeometryType(%s) IN ('LINESTRING', 'MULTILINESTRING')" % \ + compiler.process(element.clauses, **kw) + + +class Geometry_IsAreaLike(sa.sql.expression.FunctionElement[bool]): + """ Check if the geometry is a polygon or multipolygon. + """ + type = sa.Boolean() + name = 'Geometry_IsLineLike' + inherit_cache = True + + +@compiles(Geometry_IsAreaLike) # type: ignore[no-untyped-call, misc] +def _default_is_area_like(element: SaColumn, + compiler: 'sa.Compiled', **kw: Any) -> str: + return "ST_GeometryType(%s) IN ('ST_Polygon', 'ST_MultiPolygon')" % \ + compiler.process(element.clauses, **kw) + + +@compiles(Geometry_IsAreaLike, 'sqlite') # type: ignore[no-untyped-call, misc] +def _sqlite_is_area_like(element: SaColumn, + compiler: 'sa.Compiled', **kw: Any) -> str: + return "ST_GeometryType(%s) IN ('POLYGON', 'MULTIPOLYGON')" % \ + compiler.process(element.clauses, **kw) + + +class Geometry_IntersectsBbox(sa.sql.expression.FunctionElement[bool]): + """ Check if the bounding boxes of the given geometries intersect. + """ + type = sa.Boolean() + name = 'Geometry_IntersectsBbox' + inherit_cache = True + + +@compiles(Geometry_IntersectsBbox) # type: ignore[no-untyped-call, misc] +def _default_intersects(element: SaColumn, + compiler: 'sa.Compiled', **kw: Any) -> str: + arg1, arg2 = list(element.clauses) + return "%s && %s" % (compiler.process(arg1, **kw), compiler.process(arg2, **kw)) + + +@compiles(Geometry_IntersectsBbox, 'sqlite') # type: ignore[no-untyped-call, misc] +def _sqlite_intersects(element: SaColumn, + compiler: 'sa.Compiled', **kw: Any) -> str: + return "MbrIntersects(%s)" % compiler.process(element.clauses, **kw) + + class Geometry(types.UserDefinedType): # type: ignore[type-arg] """ Simplified type decorator for PostGIS geometry. This type only supports geometries in 4326 projection. @@ -82,15 +147,15 @@ class Geometry(types.UserDefinedType): # type: ignore[type-arg] class comparator_factory(types.UserDefinedType.Comparator): # type: ignore[type-arg] def intersects(self, other: SaColumn) -> 'sa.Operators': - return self.op('&&')(other) + return Geometry_IntersectsBbox(self, other) + def is_line_like(self) -> SaColumn: - return sa.func.ST_GeometryType(self, type_=sa.String).in_(('ST_LineString', - 'ST_MultiLineString')) + return Geometry_IsLineLike(self) + def is_area(self) -> SaColumn: - return sa.func.ST_GeometryType(self, type_=sa.String).in_(('ST_Polygon', - 'ST_MultiPolygon')) + return Geometry_IsAreaLike(self) def ST_DWithin(self, other: SaColumn, distance: SaColumn) -> SaColumn: @@ -119,7 +184,8 @@ class Geometry(types.UserDefinedType): # type: ignore[type-arg] def ST_ClosestPoint(self, other: SaColumn) -> SaColumn: - return sa.func.ST_ClosestPoint(self, other, type_=Geometry) + return sa.func.coalesce(sa.func.ST_ClosestPoint(self, other, type_=Geometry), + other) def ST_Buffer(self, other: SaColumn) -> SaColumn: @@ -161,11 +227,13 @@ SQLITE_FUNCTION_ALIAS = ( ('ST_AsGeoJSON', sa.Text, 'AsGeoJSON'), ('ST_AsKML', sa.Text, 'AsKML'), ('ST_AsSVG', sa.Text, 'AsSVG'), + ('ST_LineLocatePoint', sa.Float, 'ST_Line_Locate_Point'), + ('ST_LineInterpolatePoint', sa.Float, 'ST_Line_Interpolate_Point'), ) def _add_function_alias(func: str, ftype: type, alias: str) -> None: _FuncDef = type(func, (sa.sql.functions.GenericFunction, ), { - "type": ftype, + "type": ftype(), "name": func, "identifier": func, "inherit_cache": True}) @@ -181,4 +249,17 @@ for alias in SQLITE_FUNCTION_ALIAS: _add_function_alias(*alias) +class ST_DWithin(sa.sql.functions.GenericFunction[bool]): + type = sa.Boolean() + name = 'ST_DWithin' + inherit_cache = True + +@compiles(ST_DWithin, 'sqlite') # type: ignore[no-untyped-call, misc] +def default_json_array_each(element: SaColumn, compiler: 'sa.Compiled', **kw: Any) -> str: + geom1, geom2, dist = list(element.clauses) + return "(MbrIntersects(%s, ST_Expand(%s, %s)) = 1 AND ST_Distance(%s, %s) <= %s)" % ( + compiler.process(geom1, **kw), compiler.process(geom2, **kw), + compiler.process(dist, **kw), + compiler.process(geom1, **kw), compiler.process(geom2, **kw), + compiler.process(dist, **kw)) diff --git a/test/python/api/test_api_reverse.py b/test/python/api/test_api_reverse.py index d9ab2cb8..414115e1 100644 --- a/test/python/api/test_api_reverse.py +++ b/test/python/api/test_api_reverse.py @@ -16,20 +16,23 @@ import pytest import nominatim.api as napi -def test_reverse_rank_30(apiobj): +API_OPTIONS = {'reverse'} + +def test_reverse_rank_30(apiobj, frontend): apiobj.add_placex(place_id=223, class_='place', type='house', housenumber='1', centroid=(1.3, 0.7), geometry='POINT(1.3 0.7)') - result = apiobj.api.reverse((1.3, 0.7)) + api = frontend(apiobj, options=API_OPTIONS) + result = api.reverse((1.3, 0.7)) assert result is not None assert result.place_id == 223 @pytest.mark.parametrize('country', ['de', 'us']) -def test_reverse_street(apiobj, country): +def test_reverse_street(apiobj, frontend, country): apiobj.add_placex(place_id=990, class_='highway', type='service', rank_search=27, rank_address=27, name = {'name': 'My Street'}, @@ -37,17 +40,19 @@ def test_reverse_street(apiobj, country): country_code=country, geometry='LINESTRING(9.995 10, 10.005 10)') - assert apiobj.api.reverse((9.995, 10)).place_id == 990 + api = frontend(apiobj, options=API_OPTIONS) + assert api.reverse((9.995, 10)).place_id == 990 -def test_reverse_ignore_unindexed(apiobj): +def test_reverse_ignore_unindexed(apiobj, frontend): apiobj.add_placex(place_id=223, class_='place', type='house', housenumber='1', indexed_status=2, centroid=(1.3, 0.7), geometry='POINT(1.3 0.7)') - result = apiobj.api.reverse((1.3, 0.7)) + api = frontend(apiobj, options=API_OPTIONS) + result = api.reverse((1.3, 0.7)) assert result is None @@ -62,7 +67,7 @@ def test_reverse_ignore_unindexed(apiobj): (0.70003, napi.DataLayer.MANMADE | napi.DataLayer.RAILWAY, 225), (0.70003, napi.DataLayer.MANMADE | napi.DataLayer.NATURAL, 225), (5, napi.DataLayer.ADDRESS, 229)]) -def test_reverse_rank_30_layers(apiobj, y, layer, place_id): +def test_reverse_rank_30_layers(apiobj, frontend, y, layer, place_id): apiobj.add_placex(place_id=223, class_='place', type='house', housenumber='1', rank_address=30, @@ -90,21 +95,23 @@ def test_reverse_rank_30_layers(apiobj, y, layer, place_id): rank_search=30, centroid=(1.3, 5)) - assert apiobj.api.reverse((1.3, y), layers=layer).place_id == place_id + api = frontend(apiobj, options=API_OPTIONS) + assert api.reverse((1.3, y), layers=layer).place_id == place_id -def test_reverse_poi_layer_with_no_pois(apiobj): +def test_reverse_poi_layer_with_no_pois(apiobj, frontend): apiobj.add_placex(place_id=223, class_='place', type='house', housenumber='1', rank_address=30, rank_search=30, centroid=(1.3, 0.70001)) - assert apiobj.api.reverse((1.3, 0.70001), max_rank=29, + api = frontend(apiobj, options=API_OPTIONS) + assert api.reverse((1.3, 0.70001), max_rank=29, layers=napi.DataLayer.POI) is None -def test_reverse_housenumber_on_street(apiobj): +def test_reverse_housenumber_on_street(apiobj, frontend): apiobj.add_placex(place_id=990, class_='highway', type='service', rank_search=27, rank_address=27, name = {'name': 'My Street'}, @@ -116,12 +123,13 @@ def test_reverse_housenumber_on_street(apiobj): housenumber='23', centroid=(10.0, 10.00001)) - assert apiobj.api.reverse((10.0, 10.0), max_rank=30).place_id == 991 - assert apiobj.api.reverse((10.0, 10.0), max_rank=27).place_id == 990 - assert apiobj.api.reverse((10.0, 10.00001), max_rank=30).place_id == 991 + api = frontend(apiobj, options=API_OPTIONS) + assert api.reverse((10.0, 10.0), max_rank=30).place_id == 991 + assert api.reverse((10.0, 10.0), max_rank=27).place_id == 990 + assert api.reverse((10.0, 10.00001), max_rank=30).place_id == 991 -def test_reverse_housenumber_interpolation(apiobj): +def test_reverse_housenumber_interpolation(apiobj, frontend): apiobj.add_placex(place_id=990, class_='highway', type='service', rank_search=27, rank_address=27, name = {'name': 'My Street'}, @@ -138,10 +146,11 @@ def test_reverse_housenumber_interpolation(apiobj): centroid=(10.0, 10.00001), geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)') - assert apiobj.api.reverse((10.0, 10.0)).place_id == 992 + api = frontend(apiobj, options=API_OPTIONS) + assert api.reverse((10.0, 10.0)).place_id == 992 -def test_reverse_housenumber_point_interpolation(apiobj): +def test_reverse_housenumber_point_interpolation(apiobj, frontend): apiobj.add_placex(place_id=990, class_='highway', type='service', rank_search=27, rank_address=27, name = {'name': 'My Street'}, @@ -153,12 +162,13 @@ def test_reverse_housenumber_point_interpolation(apiobj): centroid=(10.0, 10.00001), geometry='POINT(10.0 10.00001)') - res = apiobj.api.reverse((10.0, 10.0)) + api = frontend(apiobj, options=API_OPTIONS) + res = api.reverse((10.0, 10.0)) assert res.place_id == 992 assert res.housenumber == '42' -def test_reverse_tiger_number(apiobj): +def test_reverse_tiger_number(apiobj, frontend): apiobj.add_placex(place_id=990, class_='highway', type='service', rank_search=27, rank_address=27, name = {'name': 'My Street'}, @@ -171,11 +181,12 @@ def test_reverse_tiger_number(apiobj): centroid=(10.0, 10.00001), geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)') - assert apiobj.api.reverse((10.0, 10.0)).place_id == 992 - assert apiobj.api.reverse((10.0, 10.00001)).place_id == 992 + api = frontend(apiobj, options=API_OPTIONS) + assert api.reverse((10.0, 10.0)).place_id == 992 + assert api.reverse((10.0, 10.00001)).place_id == 992 -def test_reverse_point_tiger(apiobj): +def test_reverse_point_tiger(apiobj, frontend): apiobj.add_placex(place_id=990, class_='highway', type='service', rank_search=27, rank_address=27, name = {'name': 'My Street'}, @@ -188,12 +199,13 @@ def test_reverse_point_tiger(apiobj): centroid=(10.0, 10.00001), geometry='POINT(10.0 10.00001)') - res = apiobj.api.reverse((10.0, 10.0)) + api = frontend(apiobj, options=API_OPTIONS) + res = api.reverse((10.0, 10.0)) assert res.place_id == 992 assert res.housenumber == '1' -def test_reverse_low_zoom_address(apiobj): +def test_reverse_low_zoom_address(apiobj, frontend): apiobj.add_placex(place_id=1001, class_='place', type='house', housenumber='1', rank_address=30, @@ -207,11 +219,12 @@ def test_reverse_low_zoom_address(apiobj): geometry="""POLYGON((59.3 80.70001, 59.3001 80.70001, 59.3001 80.70101, 59.3 80.70101, 59.3 80.70001))""") - assert apiobj.api.reverse((59.30005, 80.7005)).place_id == 1001 - assert apiobj.api.reverse((59.30005, 80.7005), max_rank=18).place_id == 1002 + api = frontend(apiobj, options=API_OPTIONS) + assert api.reverse((59.30005, 80.7005)).place_id == 1001 + assert api.reverse((59.30005, 80.7005), max_rank=18).place_id == 1002 -def test_reverse_place_node_in_area(apiobj): +def test_reverse_place_node_in_area(apiobj, frontend): apiobj.add_placex(place_id=1002, class_='place', type='town', name={'name': 'Town Area'}, rank_address=16, @@ -226,7 +239,8 @@ def test_reverse_place_node_in_area(apiobj): rank_search=18, centroid=(59.30004, 80.70055)) - assert apiobj.api.reverse((59.30004, 80.70055)).place_id == 1003 + api = frontend(apiobj, options=API_OPTIONS) + assert api.reverse((59.30004, 80.70055)).place_id == 1003 @pytest.mark.parametrize('layer,place_id', [(napi.DataLayer.MANMADE, 225), @@ -234,7 +248,7 @@ def test_reverse_place_node_in_area(apiobj): (napi.DataLayer.NATURAL, 227), (napi.DataLayer.MANMADE | napi.DataLayer.RAILWAY, 225), (napi.DataLayer.MANMADE | napi.DataLayer.NATURAL, 225)]) -def test_reverse_larger_area_layers(apiobj, layer, place_id): +def test_reverse_larger_area_layers(apiobj, frontend, layer, place_id): apiobj.add_placex(place_id=225, class_='man_made', type='dam', name={'name': 'Dam'}, rank_address=0, @@ -251,17 +265,19 @@ def test_reverse_larger_area_layers(apiobj, layer, place_id): rank_search=16, centroid=(1.3, 0.70005)) - assert apiobj.api.reverse((1.3, 0.7), layers=layer).place_id == place_id + api = frontend(apiobj, options=API_OPTIONS) + assert api.reverse((1.3, 0.7), layers=layer).place_id == place_id -def test_reverse_country_lookup_no_objects(apiobj): +def test_reverse_country_lookup_no_objects(apiobj, frontend): apiobj.add_country('xx', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))') - assert apiobj.api.reverse((0.5, 0.5)) is None + api = frontend(apiobj, options=API_OPTIONS) + assert api.reverse((0.5, 0.5)) is None @pytest.mark.parametrize('rank', [4, 30]) -def test_reverse_country_lookup_country_only(apiobj, rank): +def test_reverse_country_lookup_country_only(apiobj, frontend, rank): apiobj.add_country('xx', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))') apiobj.add_placex(place_id=225, class_='place', type='country', name={'name': 'My Country'}, @@ -270,10 +286,11 @@ def test_reverse_country_lookup_country_only(apiobj, rank): country_code='xx', centroid=(0.7, 0.7)) - assert apiobj.api.reverse((0.5, 0.5), max_rank=rank).place_id == 225 + api = frontend(apiobj, options=API_OPTIONS) + assert api.reverse((0.5, 0.5), max_rank=rank).place_id == 225 -def test_reverse_country_lookup_place_node_inside(apiobj): +def test_reverse_country_lookup_place_node_inside(apiobj, frontend): apiobj.add_country('xx', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))') apiobj.add_placex(place_id=225, class_='place', type='state', osm_type='N', @@ -283,11 +300,12 @@ def test_reverse_country_lookup_place_node_inside(apiobj): country_code='xx', centroid=(0.5, 0.505)) - assert apiobj.api.reverse((0.5, 0.5)).place_id == 225 + api = frontend(apiobj, options=API_OPTIONS) + assert api.reverse((0.5, 0.5)).place_id == 225 @pytest.mark.parametrize('gtype', list(napi.GeometryFormat)) -def test_reverse_geometry_output_placex(apiobj, gtype): +def test_reverse_geometry_output_placex(apiobj, frontend, gtype): apiobj.add_country('xx', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))') apiobj.add_placex(place_id=1001, class_='place', type='house', housenumber='1', @@ -302,34 +320,37 @@ def test_reverse_geometry_output_placex(apiobj, gtype): country_code='xx', centroid=(0.5, 0.5)) - assert apiobj.api.reverse((59.3, 80.70001), geometry_output=gtype).place_id == 1001 - assert apiobj.api.reverse((0.5, 0.5), geometry_output=gtype).place_id == 1003 + api = frontend(apiobj, options=API_OPTIONS) + assert api.reverse((59.3, 80.70001), geometry_output=gtype).place_id == 1001 + assert api.reverse((0.5, 0.5), geometry_output=gtype).place_id == 1003 -def test_reverse_simplified_geometry(apiobj): +def test_reverse_simplified_geometry(apiobj, frontend): apiobj.add_placex(place_id=1001, class_='place', type='house', housenumber='1', rank_address=30, rank_search=30, centroid=(59.3, 80.70001)) + api = frontend(apiobj, options=API_OPTIONS) details = dict(geometry_output=napi.GeometryFormat.GEOJSON, geometry_simplification=0.1) - assert apiobj.api.reverse((59.3, 80.70001), **details).place_id == 1001 + assert api.reverse((59.3, 80.70001), **details).place_id == 1001 -def test_reverse_interpolation_geometry(apiobj): +def test_reverse_interpolation_geometry(apiobj, frontend): apiobj.add_osmline(place_id=992, parent_place_id=990, startnumber=1, endnumber=3, step=1, centroid=(10.0, 10.00001), geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)') - assert apiobj.api.reverse((10.0, 10.0), geometry_output=napi.GeometryFormat.TEXT)\ + api = frontend(apiobj, options=API_OPTIONS) + assert api.reverse((10.0, 10.0), geometry_output=napi.GeometryFormat.TEXT)\ .geometry['text'] == 'POINT(10 10.00001)' -def test_reverse_tiger_geometry(apiobj): +def test_reverse_tiger_geometry(apiobj, frontend): apiobj.add_placex(place_id=990, class_='highway', type='service', rank_search=27, rank_address=27, name = {'name': 'My Street'}, @@ -342,7 +363,8 @@ def test_reverse_tiger_geometry(apiobj): centroid=(10.0, 10.00001), geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)') - output = apiobj.api.reverse((10.0, 10.0), + api = frontend(apiobj, options=API_OPTIONS) + output = api.reverse((10.0, 10.0), geometry_output=napi.GeometryFormat.GEOJSON).geometry['geojson'] assert json.loads(output) == {'coordinates': [10, 10.00001], 'type': 'Point'} -- 2.45.1