]> git.openstreetmap.org Git - nominatim.git/blobdiff - nominatim/api/types.py
Merge remote-tracking branch 'upstream/master'
[nominatim.git] / nominatim / api / types.py
index 9042e707db920ac46b2139156b3e53fcb5b6006e..e93015fcd0a8ce9cbfa67b2cb602674695b4bb11 100644 (file)
@@ -14,28 +14,49 @@ import dataclasses
 import enum
 import math
 from struct import unpack
 import enum
 import math
 from struct import unpack
-
-from geoalchemy2 import WKTElement
-import geoalchemy2.functions
+from binascii import unhexlify
 
 from nominatim.errors import UsageError
 
 from nominatim.errors import UsageError
+from nominatim.api.localization import Locales
 
 # pylint: disable=no-member,too-many-boolean-expressions,too-many-instance-attributes
 
 @dataclasses.dataclass
 class PlaceID:
 
 # pylint: disable=no-member,too-many-boolean-expressions,too-many-instance-attributes
 
 @dataclasses.dataclass
 class PlaceID:
-    """ Reference an object by Nominatim's internal ID.
+    """ Reference a place by Nominatim's internal ID.
+
+        A PlaceID may reference place from the main table placex, from
+        the interpolation tables or the postcode tables. Place IDs are not
+        stable between installations. You may use this type theefore only
+        with place IDs obtained from the same database.
     """
     place_id: int
     """
     place_id: int
+    """
+    The internal ID of the place to reference.
+    """
 
 
 @dataclasses.dataclass
 class OsmID:
 
 
 @dataclasses.dataclass
 class OsmID:
-    """ Reference by the OSM ID and potentially the basic category.
+    """ Reference a place by its OSM ID and potentially the basic category.
+
+        The OSM ID may refer to places in the main table placex and OSM
+        interpolation lines.
     """
     osm_type: str
     """
     osm_type: str
+    """ OSM type of the object. Must be one of `N`(node), `W`(way) or
+        `R`(relation).
+    """
     osm_id: int
     osm_id: int
+    """ The OSM ID of the object.
+    """
     osm_class: Optional[str] = None
     osm_class: Optional[str] = None
+    """ The same OSM object may appear multiple times in the database under
+        different categories. The optional class parameter allows to distinguish
+        the different categories and corresponds to the key part of the category.
+        If there are multiple objects in the database and `osm_class` is
+        left out, then one of the objects is returned at random.
+    """
 
     def __post_init__(self) -> None:
         if self.osm_type not in ('N', 'W', 'R'):
 
     def __post_init__(self) -> None:
         if self.osm_type not in ('N', 'W', 'R'):
@@ -73,11 +94,13 @@ class Point(NamedTuple):
 
 
     @staticmethod
 
 
     @staticmethod
-    def from_wkb(wkb: bytes) -> 'Point':
+    def from_wkb(wkb: Union[str, bytes]) -> 'Point':
         """ Create a point from EWKB as returned from the database.
         """
         """ Create a point from EWKB as returned from the database.
         """
+        if isinstance(wkb, str):
+            wkb = unhexlify(wkb)
         if len(wkb) != 25:
         if len(wkb) != 25:
-            raise ValueError("Point wkb has unexpected length")
+            raise ValueError(f"Point wkb has unexpected length {len(wkb)}")
         if wkb[0] == 0:
             gtype, srid, x, y = unpack('>iidd', wkb[1:])
         elif wkb[0] == 1:
         if wkb[0] == 0:
             gtype, srid, x, y = unpack('>iidd', wkb[1:])
         elif wkb[0] == 1:
@@ -122,10 +145,10 @@ class Point(NamedTuple):
         return Point(x, y)
 
 
         return Point(x, y)
 
 
-    def sql_value(self) -> WKTElement:
-        """ Create an SQL expression for the point.
+    def to_wkt(self) -> str:
+        """ Return the WKT representation of the point.
         """
         """
-        return WKTElement(f'POINT({self.x} {self.y})', srid=4326)
+        return f'POINT({self.x} {self.y})'
 
 
 
 
 
 
@@ -135,12 +158,15 @@ WKB_BBOX_HEADER_LE = b'\x01\x03\x00\x00\x20\xE6\x10\x00\x00\x01\x00\x00\x00\x05\
 WKB_BBOX_HEADER_BE = b'\x00\x20\x00\x00\x03\x00\x00\x10\xe6\x00\x00\x00\x01\x00\x00\x00\x05'
 
 class Bbox:
 WKB_BBOX_HEADER_BE = b'\x00\x20\x00\x00\x03\x00\x00\x10\xe6\x00\x00\x00\x01\x00\x00\x00\x05'
 
 class Bbox:
-    """ A bounding box in WSG84 projection.
+    """ A bounding box in WGS84 projection.
 
         The coordinates are available as an array in the 'coord'
         property in the order (minx, miny, maxx, maxy).
     """
     def __init__(self, minx: float, miny: float, maxx: float, maxy: float) -> None:
 
         The coordinates are available as an array in the 'coord'
         property in the order (minx, miny, maxx, maxy).
     """
     def __init__(self, minx: float, miny: float, maxx: float, maxy: float) -> None:
+        """ Create a new bounding box with the given coordinates in WGS84
+            projection.
+        """
         self.coords = (minx, miny, maxx, maxy)
 
 
         self.coords = (minx, miny, maxx, maxy)
 
 
@@ -179,12 +205,6 @@ class Bbox:
         return (self.coords[2] - self.coords[0]) * (self.coords[3] - self.coords[1])
 
 
         return (self.coords[2] - self.coords[0]) * (self.coords[3] - self.coords[1])
 
 
-    def sql_value(self) -> Any:
-        """ Create an SQL expression for the box.
-        """
-        return geoalchemy2.functions.ST_MakeEnvelope(*self.coords, 4326)
-
-
     def contains(self, pt: Point) -> bool:
         """ Check if the point is inside or on the boundary of the box.
         """
     def contains(self, pt: Point) -> bool:
         """ Check if the point is inside or on the boundary of the box.
         """
@@ -192,14 +212,25 @@ class Bbox:
                and self.coords[2] >= pt[0] and self.coords[3] >= pt[1]
 
 
                and self.coords[2] >= pt[0] and self.coords[3] >= pt[1]
 
 
+    def to_wkt(self) -> str:
+        """ Return the WKT representation of the Bbox. This
+            is a simple polygon with four points.
+        """
+        return 'POLYGON(({0} {1},{0} {3},{2} {3},{2} {1},{0} {1}))'\
+                  .format(*self.coords) # pylint: disable=consider-using-f-string
+
+
     @staticmethod
     @staticmethod
-    def from_wkb(wkb: Optional[bytes]) -> 'Optional[Bbox]':
+    def from_wkb(wkb: Union[None, str, bytes]) -> 'Optional[Bbox]':
         """ Create a Bbox from a bounding box polygon as returned by
         """ Create a Bbox from a bounding box polygon as returned by
-            the database. Return s None if the input value is None.
+            the database. Returns `None` if the input value is None.
         """
         if wkb is None:
             return None
 
         """
         if wkb is None:
             return None
 
+        if isinstance(wkb, str):
+            wkb = unhexlify(wkb)
+
         if len(wkb) != 97:
             raise ValueError("WKB must be a bounding box polygon")
         if wkb.startswith(WKB_BBOX_HEADER_LE):
         if len(wkb) != 97:
             raise ValueError("WKB must be a bounding box polygon")
         if wkb.startswith(WKB_BBOX_HEADER_LE):
@@ -242,9 +273,10 @@ class Bbox:
         except ValueError as exc:
             raise UsageError('Bounding box parameter needs to be numbers.') from exc
 
         except ValueError as exc:
             raise UsageError('Bounding box parameter needs to be numbers.') from exc
 
-        if x1 < -180.0 or x1 > 180.0 or y1 < -90.0 or y1 > 90.0 \
-           or x2 < -180.0 or x2 > 180.0 or y2 < -90.0 or y2 > 90.0:
-            raise UsageError('Bounding box coordinates invalid.')
+        x1 = min(180, max(-180, x1))
+        x2 = min(180, max(-180, x2))
+        y1 = min(90, max(-90, y1))
+        y2 = min(90, max(-90, y2))
 
         if x1 == x2 or y1 == y2:
             raise UsageError('Bounding box with invalid parameters.')
 
         if x1 == x2 or y1 == y2:
             raise UsageError('Bounding box with invalid parameters.')
@@ -253,23 +285,60 @@ class Bbox:
 
 
 class GeometryFormat(enum.Flag):
 
 
 class GeometryFormat(enum.Flag):
-    """ Geometry output formats supported by Nominatim.
+    """ All search functions support returning the full geometry of a place in
+        various formats. The internal geometry is converted by PostGIS to
+        the desired format and then returned as a string. It is possible to
+        request multiple formats at the same time.
     """
     NONE = 0
     """
     NONE = 0
+    """ No geometry requested. Alias for a empty flag.
+    """
     GEOJSON = enum.auto()
     GEOJSON = enum.auto()
+    """
+    [GeoJSON](https://geojson.org/) format
+    """
     KML = enum.auto()
     KML = enum.auto()
+    """
+    [KML](https://en.wikipedia.org/wiki/Keyhole_Markup_Language) format
+    """
     SVG = enum.auto()
     SVG = enum.auto()
+    """
+    [SVG](http://www.w3.org/TR/SVG/paths.html) format
+    """
     TEXT = enum.auto()
     TEXT = enum.auto()
+    """
+    [WKT](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry) format
+    """
 
 
 class DataLayer(enum.Flag):
 
 
 class DataLayer(enum.Flag):
-    """ Layer types that can be selected for reverse and forward search.
+    """ The `DataLayer` flag type defines the layers that can be selected
+        for reverse and forward search.
     """
     """
-    POI = enum.auto()
     ADDRESS = enum.auto()
     ADDRESS = enum.auto()
+    """ The address layer contains all places relevant for addresses:
+        fully qualified addresses with a house number (or a house name equivalent,
+        for some addresses) and places that can be part of an address like
+        roads, cities, states.
+    """
+    POI = enum.auto()
+    """ Layer for points of interest like shops, restaurants but also
+        recycling bins or postboxes.
+    """
     RAILWAY = enum.auto()
     RAILWAY = enum.auto()
-    MANMADE = enum.auto()
+    """ Layer with railway features including tracks and other infrastructure.
+        Note that in Nominatim's standard configuration, only very few railway
+        features are imported into the database. Thus a custom configuration
+        is required to make full use of this layer.
+    """
     NATURAL = enum.auto()
     NATURAL = enum.auto()
+    """ Layer with natural features like rivers, lakes and mountains.
+    """
+    MANMADE = enum.auto()
+    """ Layer with other human-made features and boundaries. This layer is
+        the catch-all and includes all features not covered by the other
+        layers. A typical example for this layer are national park boundaries.
+    """
 
 
 def format_country(cc: Any) -> List[str]:
 
 
 def format_country(cc: Any) -> List[str]:
@@ -296,16 +365,17 @@ def format_excluded(ids: Any) -> List[int]:
     """
     plist: Sequence[str]
     if isinstance(ids, str):
     """
     plist: Sequence[str]
     if isinstance(ids, str):
-        plist = ids.split(',')
+        plist = [s.strip() for s in ids.split(',')]
     elif isinstance(ids, abc.Sequence):
         plist = ids
     else:
         raise UsageError("Parameter 'excluded' needs to be a comma-separated list "
                          "or a Python list of numbers.")
     elif isinstance(ids, abc.Sequence):
         plist = ids
     else:
         raise UsageError("Parameter 'excluded' needs to be a comma-separated list "
                          "or a Python list of numbers.")
-    if any(not isinstance(i, int) or (isinstance(i, str) and not i.isdigit()) for i in plist):
+    if not all(isinstance(i, int) or
+               (isinstance(i, str) and (not i or i.isdigit())) for i in plist):
         raise UsageError("Parameter 'excluded' only takes place IDs.")
 
         raise UsageError("Parameter 'excluded' only takes place IDs.")
 
-    return [int(id) for id in plist if id]
+    return [int(id) for id in plist if id] or [0]
 
 
 def format_categories(categories: List[Tuple[str, str]]) -> List[Tuple[str, str]]:
 
 
 def format_categories(categories: List[Tuple[str, str]]) -> List[Tuple[str, str]]:
@@ -317,7 +387,7 @@ TParam = TypeVar('TParam', bound='LookupDetails') # pylint: disable=invalid-name
 
 @dataclasses.dataclass
 class LookupDetails:
 
 @dataclasses.dataclass
 class LookupDetails:
-    """ Collection of parameters that define the amount of details
+    """ Collection of parameters that define which kind of details are
         returned with a lookup or details result.
     """
     geometry_output: GeometryFormat = GeometryFormat.NONE
         returned with a lookup or details result.
     """
     geometry_output: GeometryFormat = GeometryFormat.NONE
@@ -344,6 +414,9 @@ class LookupDetails:
         0.0 means the original geometry is kept. The higher the value, the
         more the geometry gets simplified.
     """
         0.0 means the original geometry is kept. The higher the value, the
         more the geometry gets simplified.
     """
+    locales: Locales = Locales()
+    """ Preferred languages for localization of results.
+    """
 
     @classmethod
     def from_kwargs(cls: Type[TParam], kwargs: Dict[str, Any]) -> TParam:
 
     @classmethod
     def from_kwargs(cls: Type[TParam], kwargs: Dict[str, Any]) -> TParam:
@@ -400,7 +473,8 @@ class SearchDetails(LookupDetails):
                                      )
     """ Highest address rank to return.
     """
                                      )
     """ Highest address rank to return.
     """
-    layers: Optional[DataLayer] = None
+    layers: Optional[DataLayer] = dataclasses.field(default=None,
+                                                    metadata={'transform': lambda r : r})
     """ Filter which kind of data to include. When 'None' (the default) then
         filtering by layers is disabled.
     """
     """ Filter which kind of data to include. When 'None' (the default) then
         filtering by layers is disabled.
     """
@@ -411,7 +485,7 @@ class SearchDetails(LookupDetails):
     """
     excluded: List[int] = dataclasses.field(default_factory=list,
                                             metadata={'transform': format_excluded})
     """
     excluded: List[int] = dataclasses.field(default_factory=list,
                                             metadata={'transform': format_excluded})
-    """ List of OSM objects to exclude from the results. Currenlty only
+    """ List of OSM objects to exclude from the results. Currently only
         works when the internal place ID is given.
         An empty list (the default) will disable this filter.
     """
         works when the internal place ID is given.
         An empty list (the default) will disable this filter.
     """
@@ -427,7 +501,8 @@ class SearchDetails(LookupDetails):
                                               metadata={'transform': Point.from_param})
     """ Order results by distance to the given point.
     """
                                               metadata={'transform': Point.from_param})
     """ Order results by distance to the given point.
     """
-    near_radius: Optional[float] = None
+    near_radius: Optional[float] = dataclasses.field(default=None,
+                                              metadata={'transform': lambda r : r})
     """ Use near point as a filter and drop results outside the given
         radius. Radius is given in degrees WSG84.
     """
     """ Use near point as a filter and drop results outside the given
         radius. Radius is given in degrees WSG84.
     """
@@ -436,6 +511,7 @@ class SearchDetails(LookupDetails):
     """ Restrict search to places with one of the given class/type categories.
         An empty list (the default) will disable this filter.
     """
     """ Restrict search to places with one of the given class/type categories.
         An empty list (the default) will disable this filter.
     """
+    viewbox_x2: Optional[Bbox] = None
 
     def __post_init__(self) -> None:
         if self.viewbox is not None:
 
     def __post_init__(self) -> None:
         if self.viewbox is not None:
@@ -462,11 +538,13 @@ class SearchDetails(LookupDetails):
                 or (self.bounded_viewbox
                     and self.viewbox is not None and self.near is not None
                     and self.viewbox.contains(self.near))
                 or (self.bounded_viewbox
                     and self.viewbox is not None and self.near is not None
                     and self.viewbox.contains(self.near))
-                or self.layers is not None and not self.layers)
+                or (self.layers is not None and not self.layers)
+                or (self.max_rank <= 4 and
+                    self.layers is not None and not self.layers & DataLayer.ADDRESS))
 
 
     def layer_enabled(self, layer: DataLayer) -> bool:
 
 
     def layer_enabled(self, layer: DataLayer) -> bool:
-        """ Check if the given layer has been choosen. Also returns
+        """ Check if the given layer has been chosen. Also returns
             true when layer restriction has been disabled completely.
         """
         return self.layers is None or bool(self.layers & layer)
             true when layer restriction has been disabled completely.
         """
         return self.layers is None or bool(self.layers & layer)