class AdapterHTTPError(IOError):
    """An exception which must be raised by adapters when an HTTP response
    with a non-successful status code has been received.

    Base Geocoder class translates this exception to an instance of
    :class:`geopy.exc.GeocoderServiceError`.

    """

    def __init__(self, message, *, status_code, headers, text):
        """

        :param str message: Standard exception message.
        :param int status_code: HTTP status code.
        :param dict headers: HTTP response readers. A mapping object
            with lowercased or case-insensitive keys.

            .. versionadded:: 2.2
        :param str text: HTTP body text.
        """
        self.status_code = status_code
        self.headers = headers
        self.text = text
        super().__init__(message) class BaseAsyncAdapter(BaseAdapter):
    """Base class for asynchronous adapters.

    See also: :ref:`Async Mode <async_mode>`.
    """

    async def __aenter__(self):
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        pass class BaseSyncAdapter(BaseAdapter):
    """Base class for synchronous adapters.
    """

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        pass class RequestsAdapter(BaseSyncAdapter):
    """The adapter which uses `requests`_ library.

    .. _requests: https://requests.readthedocs.io

    `requests` supports keep-alives, retries, persists Cookies,
    allows response compression and uses HTTP/1.1 [currently].

    ``requests`` package must be installed in order to use this adapter.
    """

    is_available = requests_available

    def __init__(
        self,
        *,
        proxies,
        ssl_context,
        pool_connections=10,
        pool_maxsize=10,
        max_retries=2,
        pool_block=False
    ):
        if not requests_available:
            raise ImportError(
                "`requests` must be installed in order to use RequestsAdapter. "
                "If you have installed geopy via pip, you may use "
                "this command to install requests: "
                '`pip install "geopy[requests]"`.'
            )
        proxies = _normalize_proxies(proxies)
        super().__init__(proxies=proxies, ssl_context=ssl_context)

        self.session = requests.Session()
        self.session.trust_env = False  # don't use system proxies
        self.session.proxies = proxies

        self.session.mount(
            "http://",
            RequestsHTTPAdapter(
                pool_connections=pool_connections,
                pool_maxsize=pool_maxsize,
                max_retries=max_retries,
                pool_block=pool_block,
            ),
        )
        self.session.mount(
            "https://",
            RequestsHTTPWithSSLContextAdapter(
                ssl_context=ssl_context,
                pool_connections=pool_connections,
                pool_maxsize=pool_maxsize,
                max_retries=max_retries,
                pool_block=pool_block,
            ),
        )

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.session.close()

    def __del__(self):
        # Cleanup keepalive connections when Geocoder (and, thus, Adapter)
        # instances are getting garbage-collected.
        session = getattr(self, "session", None)
        if session is not None:
            try:
                session.close()
            except TypeError:
                # It's possible for the close method to try to fetch a
                # non-existent old_pool in urllib3 with a misleading state
                # ultimately due to stdlib queue/threading behaviour.
                # Since the error arises from a non-existent pool
                # (TypeError: 'NoneType' object is not callable)
                # it's safe to ignore this error
                pass

    def get_text(self, url, *, timeout, headers):
        resp = self._request(url, timeout=timeout, headers=headers)
        return resp.text

    def get_json(self, url, *, timeout, headers):
        resp = self._request(url, timeout=timeout, headers=headers)
        try:
            return resp.json()
        except ValueError:
            raise GeocoderParseError(
                "Could not deserialize using deserializer:\n%s" % resp.text
            )

    def _request(self, url, *, timeout, headers):
        try:
            resp = self.session.get(url, timeout=timeout, headers=headers)
        except Exception as error:
            message = str(error)
            if isinstance(error, SocketTimeout):
                raise GeocoderTimedOut("Service timed out")
            elif isinstance(error, SSLError):
                if "timed out" in message:
                    raise GeocoderTimedOut("Service timed out")
            elif isinstance(error, requests.ConnectionError):
                if "unauthorized" in message.lower():
                    raise GeocoderServiceError(message)
                else:
                    raise GeocoderUnavailable(message)
            elif isinstance(error, requests.Timeout):
                raise GeocoderTimedOut("Service timed out")
            raise GeocoderServiceError(message)
        else:
            if resp.status_code >= 400:
                raise AdapterHTTPError(
                    "Non-successful status code %s" % resp.status_code,
                    status_code=resp.status_code,
                    headers=resp.headers,
                    text=resp.text,
                )

        return resp class URLLibAdapter(BaseSyncAdapter):
    """The fallback adapter which uses urllib from the Python standard
    library, see :func:`urllib.request.urlopen`.

    urllib doesn't support keep-alives, request retries,
    doesn't persist Cookies and is HTTP/1.1 only.

    urllib was the only available option
    for making requests in geopy 1.x, so this adapter behaves the same
    as geopy 1.x in terms of HTTP requests.
    """

    def __init__(self, *, proxies, ssl_context):
        proxies = _normalize_proxies(proxies)
        super().__init__(proxies=proxies, ssl_context=ssl_context)

        # `ProxyHandler` should be present even when actually there're
        # no proxies. `build_opener` contains it anyway. By specifying
        # it here explicitly we can disable system proxies (i.e.
        # from HTTP_PROXY env var) by setting `proxies` to `{}`.
        # Otherwise, if we didn't specify ProxyHandler for empty
        # `proxies` here, the `build_opener` would have used one internally
        # which could have unwillingly picked up the system proxies.
        opener = build_opener(
            HTTPSHandler(context=ssl_context),
            ProxyHandler(proxies),
        )
        self.urlopen = opener.open

    def get_json(self, url, *, timeout, headers):
        text = self.get_text(url, timeout=timeout, headers=headers)
        try:
            return json.loads(text)
        except ValueError:
            raise GeocoderParseError(
                "Could not deserialize using deserializer:\n%s" % text
            )

    def get_text(self, url, *, timeout, headers):
        req = Request(url=url, headers=headers)
        try:
            page = self.urlopen(req, timeout=timeout)
        except Exception as error:
            message = str(error.args[0]) if len(error.args) else str(error)
            if isinstance(error, HTTPError):
                code = error.getcode()
                response_headers = {
                    name.lower(): value
                    for name, value in error.headers.items()
                }
                body = self._read_http_error_body(error)
                raise AdapterHTTPError(
                    message,
                    status_code=code,
                    headers=response_headers,
                    text=body,
                )
            elif isinstance(error, URLError):
                if "timed out" in message:
                    raise GeocoderTimedOut("Service timed out")
                elif "unreachable" in message:
                    raise GeocoderUnavailable("Service not available")
            elif isinstance(error, SocketTimeout):
                raise GeocoderTimedOut("Service timed out")
            elif isinstance(error, SSLError):
                if "timed out" in message:
                    raise GeocoderTimedOut("Service timed out")
            raise GeocoderServiceError(message)
        else:
            text = self._decode_page(page)
            status_code = page.getcode()
            if status_code >= 400:
                response_headers = {
                    name.lower(): value
                    for name, value in page.headers.items()
                }
                raise AdapterHTTPError(
                    "Non-successful status code %s" % status_code,
                    status_code=status_code,
                    headers=response_headers,
                    text=text,
                )

        return text

    def _read_http_error_body(self, error):
        try:
            return self._decode_page(error)
        except Exception:
            logger.debug(
                "Unable to fetch body for a non-successful HTTP response", exc_info=True
            )
            return None

    def _decode_page(self, page):
        encoding = page.headers.get_content_charset() or "utf-8"
        try:
            body_bytes = page.read()
        except Exception:
            raise GeocoderServiceError("Unable to read the response")

        try:
            return str(body_bytes, encoding=encoding)
        except ValueError:
            raise GeocoderParseError("Unable to decode the response bytes") def get_retry_after(headers):
    """Return Retry-After header value in seconds.

    .. versionadded:: 2.2
    """
    # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
    # https://github.com/urllib3/urllib3/blob/1.26.4/src/urllib3/util/retry.py#L376

    try:
        retry_after = headers['retry-after']
    except KeyError:
        return None

    if not retry_after:  # None, ''
        return None

    retry_after = retry_after.strip()

    # RFC7231 section-7.1.3:
    # Retry-After = HTTP-date / delay-seconds

    try:
        # Retry-After: 120
        seconds = int(retry_after)
    except ValueError:
        # Retry-After: Fri, 31 Dec 1999 23:59:59 GMT
        retry_date_tuple = email.utils.parsedate_tz(retry_after)
        if retry_date_tuple is None:
            logger.warning('Invalid Retry-After header: %s', retry_after)
            return None
        retry_date = email.utils.mktime_tz(retry_date_tuple)
        seconds = retry_date - time.time()

    if seconds < 0:
        seconds = 0

    return seconds class ConfigurationError(GeopyError, ValueError):
    """
    When instantiating a geocoder, the arguments given were invalid. See
    the documentation of each geocoder's ``__init__`` for more details.
    """ class GeocoderAuthenticationFailure(GeocoderServiceError):
    """
    The remote geocoding service rejected the API key or account
    credentials this geocoder was instantiated with.
    """ class GeocoderInsufficientPrivileges(GeocoderServiceError):
    """
    The remote geocoding service refused to fulfill a request using the
    account credentials given.
    """ class GeocoderQueryError(GeocoderServiceError, ValueError):
    """
    Either geopy detected input that would cause a request to fail,
    or a request was made and the remote geocoding service responded
    that the request was bad.
    """ class GeocoderQuotaExceeded(GeocoderServiceError):
    """
    The remote geocoding service refused to fulfill the request
    because the client has used its quota.
    """ class GeocoderRateLimited(GeocoderQuotaExceeded, IOError):
    """
    The remote geocoding service has rate-limited the request.
    Retrying later might help.

    Exception of this type has a ``retry_after`` attribute,
    which contains amount of time (in seconds) the service
    has asked to wait. Might be ``None`` if there were no such
    data in response.

    .. versionadded:: 2.2
    """

    def __init__(self, message, *, retry_after=None):
        super().__init__(message)
        self.retry_after = retry_after class GeocoderServiceError(GeopyError):
    """
    There was an exception caused when calling the remote geocoding service,
    and no more specific exception could be raised by geopy. When calling
    geocoders' ``geocode`` or `reverse` methods, this is the most generic
    exception that can be raised, and any non-geopy exception will be caught
    and turned into this. The exception's message will be that of the
    original exception.
    """ class GeocoderTimedOut(GeocoderServiceError, TimeoutError):
    """
    The call to the geocoding service was aborted because no response
    has been received within the ``timeout`` argument of either
    the geocoding class or, if specified, the method call.
    Some services are just consistently slow, and a higher timeout
    may be needed to use them.
    """ class Point:
    """
    A geodetic point with latitude, longitude, and altitude.

    Latitude and longitude are floating point values in degrees.
    Altitude is a floating point value in kilometers. The reference level
    is never considered and is thus application dependent, so be consistent!
    The default for all values is 0.

    Points can be created in a number of ways...

    With latitude, longitude, and altitude::

        >>> p1 = Point(41.5, -81, 0)
        >>> p2 = Point(latitude=41.5, longitude=-81)

    With a sequence of 2 to 3 values (latitude, longitude, altitude)::

        >>> p1 = Point([41.5, -81, 0])
        >>> p2 = Point((41.5, -81))

    Copy another `Point` instance::

        >>> p2 = Point(p1)
        >>> p2 == p1
        True
        >>> p2 is p1
        False

    Give a string containing at least latitude and longitude::

        >>> p = Point('41.5,-81.0')
        >>> p = Point('+41.5 -81.0')
        >>> p = Point('41.5 N -81.0 W')
        >>> p = Point('-41.5 S, 81.0 E, 2.5km')
        >>> p = Point('23 26m 22s N 23 27m 30s E 21.0mi')
        >>> p = Point('''3 26' 22" N 23 27' 30" E''')

    Point values can be accessed by name or by index::

        >>> p = Point(41.5, -81.0, 0)
        >>> p.latitude == p[0]
        True
        >>> p.longitude == p[1]
        True
        >>> p.altitude == p[2]
        True

    When unpacking (or iterating), a ``(latitude, longitude, altitude)`` tuple is
    returned::

        >>> latitude, longitude, altitude = p

    Textual representations::

        >>> p = Point(41.5, -81.0, 12.3)
        >>> str(p)  # same as `p.format()`
        '41 30m 0s N, 81 0m 0s W, 12.3km'
        >>> p.format_unicode()
        '41° 30′ 0″ N, 81° 0′ 0″ W, 12.3km'
        >>> repr(p)
        'Point(41.5, -81.0, 12.3)'
        >>> repr(tuple(p))
        '(41.5, -81.0, 12.3)'
    """

    __slots__ = ("latitude", "longitude", "altitude")

    POINT_PATTERN = POINT_PATTERN

    def __new__(cls, latitude=None, longitude=None, altitude=None):
        """
        :param float latitude: Latitude of point.
        :param float longitude: Longitude of point.
        :param float altitude: Altitude of point.
        """
        single_arg = latitude is not None and longitude is None and altitude is None
        if single_arg and not isinstance(latitude, util.NUMBER_TYPES):
            arg = latitude
            if isinstance(arg, Point):
                return cls.from_point(arg)
            elif isinstance(arg, str):
                return cls.from_string(arg)
            else:
                try:
                    seq = iter(arg)
                except TypeError:
                    raise TypeError(
                        "Failed to create Point instance from %r." % (arg,)
                    )
                else:
                    return cls.from_sequence(seq)

        if single_arg:
            raise ValueError(
                'A single number has been passed to the Point '
                'constructor. This is probably a mistake, because '
                'constructing a Point with just a latitude '
                'seems senseless. If this is exactly what was '
                'meant, then pass the zero longitude explicitly '
                'to get rid of this error.'
            )

        latitude, longitude, altitude = \
            _normalize_coordinates(latitude, longitude, altitude)

        self = super().__new__(cls)
        self.latitude = latitude
        self.longitude = longitude
        self.altitude = altitude
        return self

    def __getitem__(self, index):
        return tuple(self)[index]  # tuple handles slices

    def __setitem__(self, index, value):
        point = list(self)
        point[index] = value  # list handles slices
        self.latitude, self.longitude, self.altitude = \
            _normalize_coordinates(*point)

    def __iter__(self):
        return iter((self.latitude, self.longitude, self.altitude))

    def __getstate__(self):
        return tuple(self)

    def __setstate__(self, state):
        self.latitude, self.longitude, self.altitude = state

    def __repr__(self):
        return "Point(%r, %r, %r)" % tuple(self)

    def format(self, altitude=None, deg_char='', min_char='m', sec_char='s'):
        """
        Format decimal degrees (DD) to degrees minutes seconds (DMS)::

            >>> p = Point(41.5, -81.0, 12.3)
            >>> p.format()
            '41 30m 0s N, 81 0m 0s W, 12.3km'
            >>> p = Point(41.5, 0, 0)
            >>> p.format()
            '41 30m 0s N, 0 0m 0s E'

        See also :meth:`.format_unicode`.

        :param bool altitude: Whether to include ``altitude`` value.
            By default it is automatically included if it is non-zero.
        """
        latitude = "%s %s" % (
            format_degrees(abs(self.latitude), symbols={
                'deg': deg_char, 'arcmin': min_char, 'arcsec': sec_char
            }),
            self.latitude >= 0 and 'N' or 'S'
        )
        longitude = "%s %s" % (
            format_degrees(abs(self.longitude), symbols={
                'deg': deg_char, 'arcmin': min_char, 'arcsec': sec_char
            }),
            self.longitude >= 0 and 'E' or 'W'
        )
        coordinates = [latitude, longitude]

        if altitude is None:
            altitude = bool(self.altitude)
        if altitude:
            if not isinstance(altitude, str):
                altitude = 'km'
            coordinates.append(self.format_altitude(altitude))

        return ", ".join(coordinates)

    def format_unicode(self, altitude=None):
        """
        :meth:`.format` with pretty unicode chars for degrees,
        minutes and seconds::

            >>> p = Point(41.5, -81.0, 12.3)
            >>> p.format_unicode()
            '41° 30′ 0″ N, 81° 0′ 0″ W, 12.3km'

        :param bool altitude: Whether to include ``altitude`` value.
            By default it is automatically included if it is non-zero.
        """
        return self.format(
            altitude, DEGREE, PRIME, DOUBLE_PRIME
        )

    def format_decimal(self, altitude=None):
        """
        Format decimal degrees with altitude::

            >>> p = Point(41.5, -81.0, 12.3)
            >>> p.format_decimal()
            '41.5, -81.0, 12.3km'
            >>> p = Point(41.5, 0, 0)
            >>> p.format_decimal()
            '41.5, 0.0'

        :param bool altitude: Whether to include ``altitude`` value.
            By default it is automatically included if it is non-zero.
        """
        coordinates = [str(self.latitude), str(self.longitude)]

        if altitude is None:
            altitude = bool(self.altitude)
        if altitude:
            if not isinstance(altitude, str):
                altitude = 'km'
            coordinates.append(self.format_altitude(altitude))

        return ", ".join(coordinates)

    def format_altitude(self, unit='km'):
        """
        Format altitude with unit::

            >>> p = Point(41.5, -81.0, 12.3)
            >>> p.format_altitude()
            '12.3km'
            >>> p = Point(41.5, -81.0, 0)
            >>> p.format_altitude()
            '0.0km'

        :param str unit: Resulting altitude unit. Supported units
            are listed in :meth:`.from_string` doc.
        """
        return format_distance(self.altitude, unit=unit)

    def __str__(self):
        return self.format()

    def __eq__(self, other):
        if not isinstance(other, collections.abc.Iterable):
            return NotImplemented
        return tuple(self) == tuple(other)

    def __ne__(self, other):
        return not (self == other)

    @classmethod
    def parse_degrees(cls, degrees, arcminutes, arcseconds, direction=None):
        """
        Convert degrees, minutes, seconds and direction (N, S, E, W)
        to a single degrees number.

        :rtype: float
        """
        degrees = float(degrees)
        negative = degrees < 0
        arcminutes = float(arcminutes)
        arcseconds = float(arcseconds)

        if arcminutes or arcseconds:
            more = units.degrees(arcminutes=arcminutes, arcseconds=arcseconds)
            if negative:
                degrees -= more
            else:
                degrees += more

        if direction in [None, 'N', 'E']:
            return degrees
        elif direction in ['S', 'W']:
            return -degrees
        else:
            raise ValueError("Invalid direction! Should be one of [NSEW].")

    @classmethod
    def parse_altitude(cls, distance, unit):
        """
        Parse altitude managing units conversion::

            >>> Point.parse_altitude(712, 'm')
            0.712
            >>> Point.parse_altitude(712, 'km')
            712.0
            >>> Point.parse_altitude(712, 'mi')
            1145.852928

        :param float distance: Numeric value of altitude.
        :param str unit: ``distance`` unit. Supported units
            are listed in :meth:`.from_string` doc.
        """
        if distance is not None:
            distance = float(distance)
            CONVERTERS = {
                'km': lambda d: d,
                'm': lambda d: units.kilometers(meters=d),
                'mi': lambda d: units.kilometers(miles=d),
                'ft': lambda d: units.kilometers(feet=d),
                'nm': lambda d: units.kilometers(nautical=d),
                'nmi': lambda d: units.kilometers(nautical=d)
            }
            try:
                return CONVERTERS[unit](distance)
            except KeyError:
                raise NotImplementedError(
                    'Bad distance unit specified, valid are: %r' %
                    CONVERTERS.keys()
                )
        else:
            return distance

    @classmethod
    def from_string(cls, string):
        """
        Create and return a ``Point`` instance from a string containing
        latitude and longitude, and optionally, altitude.

        Latitude and longitude must be in degrees and may be in decimal form
        or indicate arcminutes and arcseconds (labeled with Unicode prime and
        double prime, ASCII quote and double quote or 'm' and 's'). The degree
        symbol is optional and may be included after the decimal places (in
        decimal form) and before the arcminutes and arcseconds otherwise.
        Coordinates given from south and west (indicated by S and W suffixes)
        will be converted to north and east by switching their signs. If no
        (or partial) cardinal directions are given, north and east are the
        assumed directions. Latitude and longitude must be separated by at
        least whitespace, a comma, or a semicolon (each with optional
        surrounding whitespace).

        Altitude, if supplied, must be a decimal number with given units.
        The following unit abbrevations (case-insensitive) are supported:

            - ``km`` (kilometers)
            - ``m`` (meters)
            - ``mi`` (miles)
            - ``ft`` (feet)
            - ``nm``, ``nmi`` (nautical miles)

        Some example strings that will work include:

            - ``41.5;-81.0``
            - ``41.5,-81.0``
            - ``41.5 -81.0``
            - ``41.5 N -81.0 W``
            - ``-41.5 S;81.0 E``
            - ``23 26m 22s N 23 27m 30s E``
            - ``23 26' 22" N 23 27' 30" E``
            - ``UT: N 39°20' 0'' / W 74°35' 0''``

        """
        match = re.match(cls.POINT_PATTERN, re.sub(r"''", r'"', string))
        if match:
            latitude_direction = None
            if match.group("latitude_direction_front"):
                latitude_direction = match.group("latitude_direction_front")
            elif match.group("latitude_direction_back"):
                latitude_direction = match.group("latitude_direction_back")

            longitude_direction = None
            if match.group("longitude_direction_front"):
                longitude_direction = match.group("longitude_direction_front")
            elif match.group("longitude_direction_back"):
                longitude_direction = match.group("longitude_direction_back")
            latitude = cls.parse_degrees(
                match.group('latitude_degrees') or 0.0,
                match.group('latitude_arcminutes') or 0.0,
                match.group('latitude_arcseconds') or 0.0,
                latitude_direction
            )
            longitude = cls.parse_degrees(
                match.group('longitude_degrees') or 0.0,
                match.group('longitude_arcminutes') or 0.0,
                match.group('longitude_arcseconds') or 0.0,
                longitude_direction
            )
            altitude = cls.parse_altitude(
                match.group('altitude_distance'),
                match.group('altitude_units')
            )
            return cls(latitude, longitude, altitude)
        else:
            raise ValueError(
                "Failed to create Point instance from string: unknown format."
            )

    @classmethod
    def from_sequence(cls, seq):
        """
        Create and return a new ``Point`` instance from any iterable with 2 to
        3 elements.  The elements, if present, must be latitude, longitude,
        and altitude, respectively.
        """
        args = tuple(islice(seq, 4))
        if len(args) > 3:
            raise ValueError('When creating a Point from sequence, it '
                             'must not have more than 3 items.')
        return cls(*args)

    @classmethod
    def from_point(cls, point):
        """
        Create and return a new ``Point`` instance from another ``Point``
        instance.
        """
        return cls(point.latitude, point.longitude, point.altitude) NUMBER_TYPES = (int, float, Decimal)
def pairwise(seq):
def join_filter(sep, seq, pred=bool):
def get_version():
import asyncio
import functools
import inspect
import threading
from geopy import compat
from geopy.adapters import (
    AdapterHTTPError,
    BaseAsyncAdapter,
    BaseSyncAdapter,
    RequestsAdapter,
    URLLibAdapter,
    get_retry_after,
)
from geopy.exc import (
    ConfigurationError,
    GeocoderAuthenticationFailure,
    GeocoderInsufficientPrivileges,
    GeocoderQueryError,
    GeocoderQuotaExceeded,
    GeocoderRateLimited,
    GeocoderServiceError,
    GeocoderTimedOut,
)
from geopy.point import Point
from geopy.util import __version__, logger
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.adapter.__exit__(exc_type, exc_val, exc_tb)

    async def __aenter__(self):
        """Context manager for asynchronous adapters. At exit all
        open connections will be closed.

        In asynchronous mode context manager usage is not required,
        however, it is strongly advised to avoid warnings about
        resources leaks.
        """
        if not self.__run_async:
            raise TypeError("`async with` cannot be used with sync adapters")
        res = await self.adapter.__aenter__()
        assert res is self.adapter, "adapter's __enter__ must return `self`"
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await self.adapter.__aexit__(exc_type, exc_val, exc_tb)

    def _coerce_point_to_string(self, point, output_format="%(lat)s,%(lon)s"):
        """
        Do the right thing on "point" input. For geocoders with reverse
        methods.
        """
        if not isinstance(point, Point):
            point = Point(point)

        # Altitude is silently dropped.
        #
        # Geocoding services (almost?) always consider only lat and lon
        # in queries, so altitude doesn't affect the request.
        # A non-zero altitude should not raise an exception
        # though, because PoIs are assumed to span the whole
        # altitude axis (i.e. not just the 0km plane).
        return output_format % dict(lat=point.latitude,
                                    lon=point.longitude)

    def _format_bounding_box(
        self, bbox, output_format="%(lat1)s,%(lon1)s,%(lat2)s,%(lon2)s"
    ):
        """
        Transform bounding box boundaries to a string matching
        `output_format` from the following formats:

            - [Point(lat1, lon1), Point(lat2, lon2)]
            - [[lat1, lon1], [lat2, lon2]]
            - ["lat1,lon1", "lat2,lon2"]

        It is guaranteed that lat1 <= lat2 and lon1 <= lon2.
        """
        if len(bbox) != 2:
            raise GeocoderQueryError("Unsupported format for a bounding box")
        p1, p2 = bbox
        p1, p2 = Point(p1), Point(p2)
        return output_format % dict(lat1=min(p1.latitude, p2.latitude),
                                    lon1=min(p1.longitude, p2.longitude),
                                    lat2=max(p1.latitude, p2.latitude),
                                    lon2=max(p1.longitude, p2.longitude))

    def _geocoder_exception_handler(self, error):
        """
        Geocoder-specific exceptions handler.
        Override if custom exceptions processing is needed.
        For example, raising an appropriate GeocoderQuotaExceeded on non-200
        response with a textual message in the body about the exceeded quota.

        Return `NONE_RESULT` to have the geocoding call return `None` (meaning
        empty result).
        """
        pass

    def _call_geocoder(
            self,
            url,
            callback,
            *,
            timeout=DEFAULT_SENTINEL,
            is_json=True,
            headers=None
    ):
        """
        For a generated query URL, get the results.
        """

        req_headers = self.headers.copy()
        if headers:
            req_headers.update(headers)

        timeout = (timeout if timeout is not DEFAULT_SENTINEL
                   else self.timeout)

        try:
            if is_json:
                result = self.adapter.get_json(url, timeout=timeout, headers=req_headers)
            else:
                result = self.adapter.get_text(url, timeout=timeout, headers=req_headers)
            if self.__run_async:
                async def fut():
                    try:
                        res = callback(await result)
                        if inspect.isawaitable(res):
                            res = await res
                        return res
                    except Exception as error:
                        res = self._adapter_error_handler(error)
                        if res is NONE_RESULT:
                            return None
                        raise

                return fut()
            else:
                return callback(result)
        except Exception as error:
            res = self._adapter_error_handler(error)
            if res is NONE_RESULT:
                return None
            raise

    def _adapter_error_handler(self, error):

        if isinstance(error, AdapterHTTPError):