text: from functools import partial
  from urllib.parse import urlencode
  
  from geopy.exc import (
      GeocoderAuthenticationFailure,
      GeocoderInsufficientPrivileges,
      GeocoderQueryError,
      GeocoderQuotaExceeded,
      GeocoderServiceError,
  )
  from geopy.geocoders.base import DEFAULT_SENTINEL, Geocoder
  from geopy.location import Location
  from geopy.timezone import (
      ensure_pytz_is_installed,
      from_fixed_gmt_offset,
      from_timezone_name,
  )
  from geopy.util import logger
  
  __all__ = ("GeoNames", )
  
  
  class GeoNames(Geocoder):
      """GeoNames geocoder.
  
      Documentation at:
          http://www.geonames.org/export/geonames-search.html
  
      Reverse geocoding documentation at:
          http://www.geonames.org/export/web-services.html#findNearbyPlaceName
      """
  
      geocode_path = '/searchJSON'
      reverse_path = '/findNearbyPlaceNameJSON'
      reverse_nearby_path = '/findNearbyJSON'
      timezone_path = '/timezoneJSON'
  
      def __init__(
              self,
              username,
              *,
              timeout=DEFAULT_SENTINEL,
              proxies=DEFAULT_SENTINEL,
              user_agent=None,
              ssl_context=DEFAULT_SENTINEL,
              adapter_factory=None,
              scheme='http'
      ):
          """
  
          :param str username: GeoNames username, required. Sign up here:
              http://www.geonames.org/login
  
          :param int timeout:
              See :attr:`geopy.geocoders.options.default_timeout`.
  
          :param dict proxies:
              See :attr:`geopy.geocoders.options.default_proxies`.
  
          :param str user_agent:
              See :attr:`geopy.geocoders.options.default_user_agent`.
  
          :type ssl_context: :class:`ssl.SSLContext`
          :param ssl_context:
              See :attr:`geopy.geocoders.options.default_ssl_context`.
  
          :param callable adapter_factory:
              See :attr:`geopy.geocoders.options.default_adapter_factory`.
  
              .. versionadded:: 2.0
  
          :param str scheme:
              See :attr:`geopy.geocoders.options.default_scheme`. Note that
              at the time of writing GeoNames doesn't support `https`, so
              the default scheme is `http`. The value of
              :attr:`geopy.geocoders.options.default_scheme` is not respected.
              This parameter is present to make it possible to switch to
              `https` once GeoNames adds support for it.
          """
          super().__init__(
              scheme=scheme,
              timeout=timeout,
              proxies=proxies,
              user_agent=user_agent,
              ssl_context=ssl_context,
              adapter_factory=adapter_factory,
          )
          self.username = username
  
          domain = 'api.geonames.org'
          self.api = (
              "%s://%s%s" % (self.scheme, domain, self.geocode_path)
          )
          self.api_reverse = (
              "%s://%s%s" % (self.scheme, domain, self.reverse_path)
          )
          self.api_reverse_nearby = (
              "%s://%s%s" % (self.scheme, domain, self.reverse_nearby_path)
          )
          self.api_timezone = (
              "%s://%s%s" % (self.scheme, domain, self.timezone_path)
          )
  
      def geocode(
              self,
              query,
              *,
              exactly_one=True,
              timeout=DEFAULT_SENTINEL,
              country=None,
              country_bias=None
      ):
          """
          Return a location point by address.
  
          :param str query: The address or query you wish to geocode.
  
          :param bool exactly_one: Return one result or a list of results, if
              available.
  
          :param int timeout: Time, in seconds, to wait for the geocoding service
              to respond before raising a :class:`geopy.exc.GeocoderTimedOut`
              exception. Set this only if you wish to override, on this call
              only, the value set during the geocoder's initialization.
  
          :param country: Limit records to the specified countries.
              Two letter country code ISO-3166 (e.g. ``FR``). Might be
              a single string or a list of strings.
          :type country: str or list
  
          :param str country_bias: Records from the country_bias are listed first.
              Two letter country code ISO-3166.
  
          :rtype: ``None``, :class:`geopy.location.Location` or a list of them, if
              ``exactly_one=False``.
          """
          params = [
              ('q', query),
              ('username', self.username),
          ]
  
          if country_bias:
              params.append(('countryBias', country_bias))
  
          if not country:
              country = []
          if isinstance(country, str):
              country = [country]
          for country_item in country:
              params.append(('country', country_item))
  
          if exactly_one:
              params.append(('maxRows', 1))
          url = "?".join((self.api, urlencode(params)))
          logger.debug("%s.geocode: %s", self.__class__.__name__, url)
          callback = partial(self._parse_json, exactly_one=exactly_one)
          return self._call_geocoder(url, callback, timeout=timeout)
  
      def reverse(
              self,
              query,
              *,
              exactly_one=True,
              timeout=DEFAULT_SENTINEL,
              feature_code=None,
              lang=None,
              find_nearby_type='findNearbyPlaceName'
      ):
          """
          Return an address by location point.
  
          :param query: The coordinates for which you wish to obtain the
              closest human-readable addresses.
          :type query: :class:`geopy.point.Point`, list or tuple of ``(latitude,
              longitude)``, or string as ``"%(latitude)s, %(longitude)s"``.
  
          :param bool exactly_one: Return one result or a list of results, if
              available.
  
          :param int timeout: Time, in seconds, to wait for the geocoding service
              to respond before raising a :class:`geopy.exc.GeocoderTimedOut`
              exception. Set this only if you wish to override, on this call
              only, the value set during the geocoder's initialization.
  
          :param str feature_code: A GeoNames feature code
  
          :param str lang: language of the returned ``name`` element (the pseudo
              language code 'local' will return it in local language)
              Full list of supported languages can be found here:
              https://www.geonames.org/countries/
  
          :param str find_nearby_type: A flag to switch between different
              GeoNames API endpoints. The default value is ``findNearbyPlaceName``
              which returns the closest populated place. Another currently
              implemented option is ``findNearby`` which returns
              the closest toponym for the lat/lng query.
  
          :rtype: ``None``, :class:`geopy.location.Location` or a list of them, if
              ``exactly_one=False``.
  
          """
  
          try:
              lat, lng = self._coerce_point_to_string(query).split(',')
          except ValueError:
              raise ValueError("Must be a coordinate pair or Point")
  
          if find_nearby_type == 'findNearbyPlaceName':  # default
              if feature_code:
                  raise ValueError(
                      "find_nearby_type=findNearbyPlaceName doesn't support "
                      "the `feature_code` param"
                  )
              params = self._reverse_find_nearby_place_name_params(
                  lat=lat,
                  lng=lng,
                  lang=lang,
              )
              url = "?".join((self.api_reverse, urlencode(params)))
          elif find_nearby_type == 'findNearby':
              if lang:
                  raise ValueError(
                      "find_nearby_type=findNearby doesn't support the `lang` param"
                  )
              params = self._reverse_find_nearby_params(
                  lat=lat,
                  lng=lng,
                  feature_code=feature_code,
              )
              url = "?".join((self.api_reverse_nearby, urlencode(params)))
          else:
              raise GeocoderQueryError(
                  '`%s` find_nearby_type is not supported by geopy' % find_nearby_type
              )
  
          logger.debug("%s.reverse: %s", self.__class__.__name__, url)
          callback = partial(self._parse_json, exactly_one=exactly_one)
          return self._call_geocoder(url, callback, timeout=timeout)
  
      def _reverse_find_nearby_params(self, lat, lng, feature_code):
          params = {
              'lat': lat,
              'lng': lng,
              'username': self.username,
          }
          if feature_code:
              params['featureCode'] = feature_code
          return params
  
      def _reverse_find_nearby_place_name_params(self, lat, lng, lang):
          params = {
              'lat': lat,
              'lng': lng,
              'username': self.username,
          }
          if lang:
              params['lang'] = lang
          return params
  
      def reverse_timezone(self, query, *, timeout=DEFAULT_SENTINEL):
          """
          Find the timezone for a point in `query`.
  
          GeoNames always returns a timezone: if the point being queried
          doesn't have an assigned Olson timezone id, a ``pytz.FixedOffset``
          timezone is used to produce the :class:`geopy.timezone.Timezone`.
  
          :param query: The coordinates for which you want a timezone.
          :type query: :class:`geopy.point.Point`, list or tuple of (latitude,
              longitude), or string as "%(latitude)s, %(longitude)s"
  
          :param int timeout: Time, in seconds, to wait for the geocoding service
              to respond before raising a :class:`geopy.exc.GeocoderTimedOut`
              exception. Set this only if you wish to override, on this call
              only, the value set during the geocoder's initialization.
  
          :rtype: :class:`geopy.timezone.Timezone`.
          """
          ensure_pytz_is_installed()
  
          try:
              lat, lng = self._coerce_point_to_string(query).split(',')
          except ValueError:
              raise ValueError("Must be a coordinate pair or Point")
  
          params = {
              "lat": lat,
              "lng": lng,
              "username": self.username,
          }
  
          url = "?".join((self.api_timezone, urlencode(params)))
  
          logger.debug("%s.reverse_timezone: %s", self.__class__.__name__, url)
          return self._call_geocoder(url, self._parse_json_timezone, timeout=timeout)
  
      def _raise_for_error(self, body):
          err = body.get('status')
          if err:
              code = err['value']
              message = err['message']
              # http://www.geonames.org/export/webservice-exception.html
              if message.startswith("user account not enabled to use"):
                  raise GeocoderInsufficientPrivileges(message)
              if code == 10:
                  raise GeocoderAuthenticationFailure(message)
              if code in (18, 19, 20):
                  raise GeocoderQuotaExceeded(message)
              raise GeocoderServiceError(message)
  
      def _parse_json_timezone(self, response):
          self._raise_for_error(response)
  
          timezone_id = response.get("timezoneId")
          if timezone_id is None:
              # Sometimes (e.g. for Antarctica) GeoNames doesn't return
              # a `timezoneId` value, but it returns GMT offsets.
              # Apparently GeoNames always returns these offsets -- for
              # every single point on the globe.
              raw_offset = response["rawOffset"]
              return from_fixed_gmt_offset(raw_offset, raw=response)
          else:
              return from_timezone_name(timezone_id, raw=response)
  
      def _parse_json(self, doc, exactly_one):
          """
          Parse JSON response body.
          """
          places = doc.get('geonames', [])
          self._raise_for_error(doc)
          if not len(places):
              return None
  
          def parse_code(place):
              """
              Parse each record.
              """
              latitude = place.get('lat', None)
              longitude = place.get('lng', None)
              if latitude and longitude:
                  latitude = float(latitude)
                  longitude = float(longitude)
              else:
                  return None
  
              placename = place.get('name')
              state = place.get('adminName1', None)
              country = place.get('countryName', None)
  
              location = ', '.join(
                  [x for x in [placename, state, country] if x]
              )
  
              return Location(location, (latitude, longitude), place)
  
          if exactly_one:
              return parse_code(places[0])
          else:
              return [parse_code(place) for place in places]
  