From: Sarah Hoffmann Date: Mon, 3 Apr 2023 08:40:38 +0000 (+0200) Subject: python lookup: add function for simple lookups X-Git-Tag: v4.3.0~83^2~4 X-Git-Url: https://git.openstreetmap.org./nominatim.git/commitdiff_plain/4607c7ed04188e8e558949553b5c84fb86f351d0?ds=sidebyside;hp=63638eb44745fc2e2aa4803d01ac868dd04a36a9 python lookup: add function for simple lookups --- diff --git a/nominatim/api/lookup.py b/nominatim/api/lookup.py index 5c54da4a..82352702 100644 --- a/nominatim/api/lookup.py +++ b/nominatim/api/lookup.py @@ -12,7 +12,7 @@ import datetime as dt import sqlalchemy as sa -from nominatim.typing import SaColumn, SaLabel, SaRow +from nominatim.typing import SaColumn, SaRow, SaSelect from nominatim.api.connection import SearchConnection import nominatim.api.types as ntyp import nominatim.api.results as nres @@ -20,23 +20,12 @@ from nominatim.api.logging import log RowFunc = Callable[[Optional[SaRow], Type[nres.BaseResultT]], Optional[nres.BaseResultT]] -def _select_column_geometry(column: SaColumn, - geometry_output: ntyp.GeometryFormat) -> SaLabel: - """ Create the appropriate column expression for selecting a - geometry for the details response. - """ - if geometry_output & ntyp.GeometryFormat.GEOJSON: - return sa.literal_column(f""" - ST_AsGeoJSON(CASE WHEN ST_NPoints({column.name}) > 5000 - THEN ST_SimplifyPreserveTopology({column.name}, 0.0001) - ELSE {column.name} END) - """).label('geometry_geojson') +GeomFunc = Callable[[SaSelect, SaColumn], SaSelect] - return sa.func.ST_GeometryType(column).label('geometry_type') async def find_in_placex(conn: SearchConnection, place: ntyp.PlaceRef, - details: ntyp.LookupDetails) -> Optional[SaRow]: + add_geometries: GeomFunc) -> Optional[SaRow]: """ Search for the given place in the placex table and return the base information. """ @@ -49,8 +38,7 @@ async def find_in_placex(conn: SearchConnection, place: ntyp.PlaceRef, t.c.importance, t.c.wikipedia, t.c.indexed_date, t.c.parent_place_id, t.c.rank_address, t.c.rank_search, t.c.linked_place_id, - t.c.centroid, - _select_column_geometry(t.c.geometry, details.geometry_output)) + t.c.centroid) if isinstance(place, ntyp.PlaceID): sql = sql.where(t.c.place_id == place.place_id) @@ -65,11 +53,11 @@ async def find_in_placex(conn: SearchConnection, place: ntyp.PlaceRef, else: return None - return (await conn.execute(sql)).one_or_none() + return (await conn.execute(add_geometries(sql, t.c.geometry))).one_or_none() async def find_in_osmline(conn: SearchConnection, place: ntyp.PlaceRef, - details: ntyp.LookupDetails) -> Optional[SaRow]: + add_geometries: GeomFunc) -> Optional[SaRow]: """ Search for the given place in the osmline table and return the base information. """ @@ -78,8 +66,7 @@ async def find_in_osmline(conn: SearchConnection, place: ntyp.PlaceRef, sql = sa.select(t.c.place_id, t.c.osm_id, t.c.parent_place_id, t.c.indexed_date, t.c.startnumber, t.c.endnumber, t.c.step, t.c.address, t.c.postcode, t.c.country_code, - t.c.linegeo.ST_Centroid().label('centroid'), - _select_column_geometry(t.c.linegeo, details.geometry_output)) + t.c.linegeo.ST_Centroid().label('centroid')) if isinstance(place, ntyp.PlaceID): sql = sql.where(t.c.place_id == place.place_id) @@ -94,14 +81,17 @@ async def find_in_osmline(conn: SearchConnection, place: ntyp.PlaceRef, else: return None - return (await conn.execute(sql)).one_or_none() + return (await conn.execute(add_geometries(sql, t.c.linegeo))).one_or_none() async def find_in_tiger(conn: SearchConnection, place: ntyp.PlaceRef, - details: ntyp.LookupDetails) -> Optional[SaRow]: + add_geometries: GeomFunc) -> Optional[SaRow]: """ Search for the given place in the table of Tiger addresses and return the base information. Only lookup by place ID is supported. """ + if not isinstance(place, ntyp.PlaceID): + return None + log().section("Find in TIGER table") t = conn.t.tiger parent = conn.t.placex @@ -109,61 +99,54 @@ async def find_in_tiger(conn: SearchConnection, place: ntyp.PlaceRef, parent.c.osm_type, parent.c.osm_id, t.c.startnumber, t.c.endnumber, t.c.step, t.c.postcode, - t.c.linegeo.ST_Centroid().label('centroid'), - _select_column_geometry(t.c.linegeo, details.geometry_output)) - - if isinstance(place, ntyp.PlaceID): - sql = sql.where(t.c.place_id == place.place_id)\ - .join(parent, t.c.parent_place_id == parent.c.place_id, isouter=True) - else: - return None + t.c.linegeo.ST_Centroid().label('centroid'))\ + .where(t.c.place_id == place.place_id)\ + .join(parent, t.c.parent_place_id == parent.c.place_id, isouter=True) - return (await conn.execute(sql)).one_or_none() + return (await conn.execute(add_geometries(sql, t.c.linegeo))).one_or_none() async def find_in_postcode(conn: SearchConnection, place: ntyp.PlaceRef, - details: ntyp.LookupDetails) -> Optional[SaRow]: + add_geometries: GeomFunc) -> Optional[SaRow]: """ Search for the given place in the postcode table and return the base information. Only lookup by place ID is supported. """ + if not isinstance(place, ntyp.PlaceID): + return None + log().section("Find in postcode table") t = conn.t.postcode sql = sa.select(t.c.place_id, t.c.parent_place_id, t.c.rank_search, t.c.rank_address, t.c.indexed_date, t.c.postcode, t.c.country_code, - t.c.geometry.label('centroid'), - _select_column_geometry(t.c.geometry, details.geometry_output)) + t.c.geometry.label('centroid')) \ + .where(t.c.place_id == place.place_id) - if isinstance(place, ntyp.PlaceID): - sql = sql.where(t.c.place_id == place.place_id) - else: - return None - - return (await conn.execute(sql)).one_or_none() + return (await conn.execute(add_geometries(sql, t.c.geometry))).one_or_none() async def find_in_all_tables(conn: SearchConnection, place: ntyp.PlaceRef, - details: ntyp.LookupDetails + add_geometries: GeomFunc ) -> Tuple[Optional[SaRow], RowFunc[nres.BaseResultT]]: """ Search for the given place in all data tables and return the base information. """ - row = await find_in_placex(conn, place, details) + row = await find_in_placex(conn, place, add_geometries) log().var_dump('Result (placex)', row) if row is not None: return row, nres.create_from_placex_row - row = await find_in_osmline(conn, place, details) + row = await find_in_osmline(conn, place, add_geometries) log().var_dump('Result (osmline)', row) if row is not None: return row, nres.create_from_osmline_row - row = await find_in_postcode(conn, place, details) + row = await find_in_postcode(conn, place, add_geometries) log().var_dump('Result (postcode)', row) if row is not None: return row, nres.create_from_postcode_row - row = await find_in_tiger(conn, place, details) + row = await find_in_tiger(conn, place, add_geometries) log().var_dump('Result (tiger)', row) return row, nres.create_from_tiger_row @@ -172,13 +155,24 @@ async def get_detailed_place(conn: SearchConnection, place: ntyp.PlaceRef, details: ntyp.LookupDetails) -> Optional[nres.DetailedResult]: """ Retrieve a place with additional details from the database. """ - log().function('get_place_by_id', place=place, details=details) + log().function('get_detailed_place', place=place, details=details) if details.geometry_output and details.geometry_output != ntyp.GeometryFormat.GEOJSON: raise ValueError("lookup only supports geojosn polygon output.") + if details.geometry_output & ntyp.GeometryFormat.GEOJSON: + def _add_geometry(sql: SaSelect, column: SaColumn) -> SaSelect: + return sql.add_columns(sa.literal_column(f""" + ST_AsGeoJSON(CASE WHEN ST_NPoints({column.name}) > 5000 + THEN ST_SimplifyPreserveTopology({column.name}, 0.0001) + ELSE {column.name} END) + """).label('geometry_geojson')) + else: + def _add_geometry(sql: SaSelect, column: SaColumn) -> SaSelect: + return sql.add_columns(sa.func.ST_GeometryType(column).label('geometry_type')) + row_func: RowFunc[nres.DetailedResult] - row, row_func = await find_in_all_tables(conn, place, details) + row, row_func = await find_in_all_tables(conn, place, _add_geometry) if row is None: return None @@ -198,3 +192,48 @@ async def get_detailed_place(conn: SearchConnection, place: ntyp.PlaceRef, await nres.add_result_details(conn, result, details) return result + + +async def get_simple_place(conn: SearchConnection, place: ntyp.PlaceRef, + details: ntyp.LookupDetails) -> Optional[nres.SearchResult]: + """ Retrieve a place as a simple search result from the database. + """ + log().function('get_simple_place', place=place, details=details) + + def _add_geometry(sql: SaSelect, col: SaColumn) -> SaSelect: + if not details.geometry_output: + return sql + + out = [] + + if details.geometry_simplification > 0.0: + col = col.ST_SimplifyPreserveTopology(details.geometry_simplification) + + if details.geometry_output & ntyp.GeometryFormat.GEOJSON: + out.append(col.ST_AsGeoJSON().label('geometry_geojson')) + if details.geometry_output & ntyp.GeometryFormat.TEXT: + out.append(col.ST_AsText().label('geometry_text')) + if details.geometry_output & ntyp.GeometryFormat.KML: + out.append(col.ST_AsKML().label('geometry_kml')) + if details.geometry_output & ntyp.GeometryFormat.SVG: + out.append(col.ST_AsSVG().label('geometry_svg')) + + return sql.add_columns(*out) + + + row_func: RowFunc[nres.SearchResult] + row, row_func = await find_in_all_tables(conn, place, _add_geometry) + + if row is None: + return None + + result = row_func(row, nres.SearchResult) + assert result is not None + + # add missing details + assert result is not None + result.bbox = getattr(row, 'bbox', None) + + await nres.add_result_details(conn, result, details) + + return result diff --git a/nominatim/api/results.py b/nominatim/api/results.py index 098851ef..98b13380 100644 --- a/nominatim/api/results.py +++ b/nominatim/api/results.py @@ -173,6 +173,19 @@ class ReverseResults(List[ReverseResult]): """ +@dataclasses.dataclass +class SearchResult(BaseResult): + """ A search result for forward geocoding. + """ + bbox: Optional[Bbox] = None + + +class SearchResults(List[SearchResult]): + """ Sequence of forward lookup results ordered by relevance. + May be empty when no result was found. + """ + + def _filter_geometries(row: SaRow) -> Dict[str, str]: return {k[9:]: v for k, v in row._mapping.items() # pylint: disable=W0212 if k.startswith('geometry_')}