Ejemplo n.º 1
0
 def test_pro_sort_AttrSorter(self):
     """Test AttrSorter postprocessor."""
     candidates_in = [self.better, self.best, self.good]
     candidates_exp = [self.good, self.better, self.best]
     candidates_out = AttrSorter(
         self.locators_worse_to_better).process(candidates_in)
     self.assertEqual_(candidates_out, candidates_exp)
Ejemplo n.º 2
0
class _EsriNAGeocodeService():
    """
    Defaults for the _EsriNAGeocodeService
    """

    LOCATOR_MAP = {
        'RoofTop': 'rooftop',
        'Streets': 'interpolation',
    }

    DEFAULT_PREPROCESSORS = [CountryPreProcessor(['US', 'CA'])]

    DEFAULT_POSTPROCESSORS = [
        AttrRename('locator', LOCATOR_MAP),
        AttrFilter(['rooftop', 'interpolation'], 'locator'),
        AttrSorter(['rooftop', 'interpolation'], 'locator'),
        UseHighScoreIfAtLeast(99.8),
        GroupBy('match_addr'),
        ScoreSorter(),
    ]
Ejemplo n.º 3
0
class EsriWGS(GeocodeService):
    """
    Class to geocode using the `ESRI World Geocoding service
    <http://geocode.arcgis.com/arcgis/geocoding.html>`_.

    This uses two endpoints -- one for single-line addresses,
    and one for multi-part addresses.

    An optional (key) parameter can be passed to the PlaceQuery
    which will be passed as a magicKey to the find endpoint if
    using a single line address/text search. This allows omgeo
    to be used with the `Esri suggest endpoint
    <https://developers.arcgis.com/rest/geocode/api-reference/geocoding-suggest.htm>`_.

    Note: Based on tests using the magicKey parameter, it is
    recommended that a viewbox not be used with in conjuction
    with the magicKey. Additionally, address/search text passed
    via the query may be ignored when using a magicKey.
    """

    LOCATOR_MAP = {
        'PointAddress': 'rooftop',
        'StreetAddress': 'interpolation',
        'PostalExt': 'postal_specific',  # accept ZIP+4
        'Postal': 'postal'
    }

    DEFAULT_PREPROCESSORS = [CancelIfPOBox()]

    DEFAULT_POSTPROCESSORS = [
        AttrFilter(
            [
                'PointAddress',
                'StreetAddress',
                #'PostalExt',
                #'Postal'
            ],
            'locator_type'),
        #AttrExclude(['USA_Postal'], 'locator'), #accept postal from everywhere but US (need PostalExt)
        AttrSorter(
            [
                'PointAddress',
                'StreetAddress',
                #'PostalExt',
                #'Postal'
            ],
            'locator_type'),
        AttrRename(
            'locator',
            LOCATOR_MAP),  # after filter to avoid searching things we toss out
        UseHighScoreIfAtLeast(99.8),
        ScoreSorter(),
        GroupBy('match_addr'),
        GroupBy(('x', 'y')),
    ]

    _endpoint = 'http://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer'

    def _geocode(self, pq):
        """
        :arg PlaceQuery pq: PlaceQuery object to use for geocoding
        :returns: list of location Candidates
        """
        #: List of desired output fields
        #: See `ESRI docs <http://geocode.arcgis.com/arcgis/geocoding.html#output>_` for details
        outFields = (
            'Loc_name',
            #'Shape',
            'Score',
            'Match_Addr',  #based on address standards for the country
            #'Address', # returned by default
            #'Country' # 3-digit ISO 3166-1 code for a country. Example: Canada = "CAN"
            #'Admin',
            #'DepAdmin',
            #'SubAdmin',
            #'Locality',
            #'Postal',
            #'PostalExt',
            'Addr_Type',
            #'Type',
            #'Rank',
            'AddNum',
            'StPreDir',
            'StPreType',
            'StName',
            'StType',
            'StDir',
            #'Side',
            #'AddNumFrom',
            #'AddNumTo',
            #'AddBldg',
            'City',
            'Subregion',
            'Region',
            'Postal',
            'Country',
            #'Ymax',
            #'Ymin',
            #'Xmin',
            #'Xmax',
            #'X',
            #'Y',
            'DisplayX',
            'DisplayY',
            #'LangCode',
            #'Status',
        )
        outFields = ','.join(outFields)
        query = dict(
            f='json',  # default HTML. Other options are JSON and KMZ.
            outFields=outFields,
            #outSR=WKID, defaults to 4326
            maxLocations=20,  # default 1; max is 20
        )

        # Postal-code only searches work in the single-line but not multipart geocoder
        # Remember that with the default postprocessors, postcode-level results will be eliminated
        if pq.query == pq.address == '' and pq.postal != '':
            pq.query = pq.postal

        if pq.query == '':  # multipart
            method = 'findAddressCandidates'
            query = dict(
                query,
                Address=pq.
                address,  # commonly represents the house number and street name of a complete address
                Neighborhood=pq.neighborhood,
                City=pq.city,
                Subregion=pq.subregion,
                Region=pq.state,
                Postal=pq.postal,
                #PostalExt=
                CountryCode=pq.
                country,  # full country name or ISO 3166-1 2- or 3-digit country code
            )
            if pq.bounded and pq.viewbox is not None:
                query = dict(query, searchExtent=pq.viewbox.to_esri_wgs_json())
        else:  # single-line
            method = 'find'
            magic_key = pq.key if hasattr(pq, 'key') else ''
            query = dict(
                query,
                text=pq.
                query,  # This can be a street address, place name, postal code, or POI.
                sourceCountry=pq.
                country,  # full country name or ISO 3166-1 2- or 3-digit country code
            )
            if magic_key:
                query[
                    'magicKey'] = magic_key  # This is a lookup key returned from the suggest endpoint.
            if pq.bounded and pq.viewbox is not None:
                query = dict(query, bbox=pq.viewbox.to_esri_wgs_json())

        endpoint = self._endpoint + '/' + method
        response_obj = self._get_json_obj(endpoint, query)
        returned_candidates = []  # this will be the list returned
        try:
            if method == 'find':
                locations = response_obj['locations']
            else:
                locations = response_obj['candidates']

            for location in locations:
                c = Candidate()
                if method == 'find':  # singlepart
                    attributes = location['feature']['attributes']
                else:  # findAddressCandidates / multipart
                    attributes = location['attributes']
                c.match_addr = attributes['Match_Addr']
                c.locator = attributes['Loc_name']
                c.locator_type = attributes['Addr_Type']
                c.score = attributes['Score']
                c.x = attributes[
                    'DisplayX']  # represents the actual location of the address.
                c.y = attributes['DisplayY']
                c.wkid = response_obj['spatialReference']['wkid']
                c.geoservice = self.__class__.__name__

                # Optional address component fields.
                for in_key, out_key in [('City', 'match_city'),
                                        ('Subregion', 'match_subregion'),
                                        ('Region', 'match_region'),
                                        ('Postal', 'match_postal'),
                                        ('Country', 'match_country')]:
                    setattr(c, out_key, attributes.get(in_key, ''))
                setattr(c, 'match_streetaddr',
                        self._street_addr_from_response(attributes))
                returned_candidates.append(c)
        except KeyError:
            pass
        return returned_candidates

    def _street_addr_from_response(self, attributes):
        """Construct a street address (no city, region, etc.) from a geocoder response.

        :param attributes: A dict of address attributes as returned by the Esri geocoder.
        """
        # The exact ordering of the address component fields that should be
        # used to reconstruct the full street address is not specified in the
        # Esri documentation, but the examples imply that it is this.
        ordered_fields = [
            'AddNum', 'StPreDir', 'StPreType', 'StName', 'StType', 'StDir'
        ]
        result = []
        for field in ordered_fields:
            result.append(attributes.get(field, ''))
        if any(result):
            return ' '.join([s for s in result
                             if s])  # Filter out empty strings.
        else:
            return ''

    def __init__(self, preprocessors=None, postprocessors=None, settings=None):
        preprocessors = EsriWGS.DEFAULT_PREPROCESSORS if preprocessors is None else preprocessors
        postprocessors = EsriWGS.DEFAULT_POSTPROCESSORS if postprocessors is None else postprocessors
        GeocodeService.__init__(self, preprocessors, postprocessors, settings)
Ejemplo n.º 4
0
class Bing(GeocodeService):
    """
    Class to geocode using Bing services:
     * `Find a Location by Query <http://msdn.microsoft.com/en-us/library/ff701711.aspx>`_
     * `Find a Location by Address <http://msdn.microsoft.com/en-us/library/ff701714.aspx>`_

    Settings used by the Bing GeocodeService object may include:
     * api_key --  The API key used to access Bing services.

    """
    _endpoint = 'http://dev.virtualearth.net/REST/v1/Locations'

    DEFAULT_PREPROCESSORS = [ReplaceRangeWithNumber()]

    DEFAULT_POSTPROCESSORS = [
        AttrMigrator('confidence', 'score', {
            'High': 100,
            'Medium': 85,
            'Low': 50
        }),
        UseHighScoreIfAtLeast(100),
        AttrFilter([
            'Address', 'AdministrativeBuilding', 'AgriculturalStructure',
            'BusinessName', 'BusinessStructure', 'BusStation', 'Camp',
            'Church', 'CityHall', 'CommunityCenter', 'ConventionCenter',
            'Courthouse', 'Factory', 'FerryTerminal', 'FishHatchery', 'Fort',
            'Garden', 'Geyser', 'Heliport', 'IndustrialStructure',
            'InformationCenter', 'Junction', 'LandmarkBuilding', 'Library',
            'Lighthouse', 'Marina', 'MedicalStructure', 'MetroStation', 'Mine',
            'Mission', 'Monument', 'Mosque', 'Museum', 'NauticalStructure',
            'NavigationalStructure', 'OfficeBuilding', 'ParkAndRide',
            'PlayingField', 'PoliceStation', 'PostOffice', 'PowerStation',
            'Prison', 'RaceTrack', 'ReligiousStructure', 'RestArea', 'Ruin',
            'ShoppingCenter', 'Site', 'SkiArea', 'Spring', 'Stadium', 'Temple',
            'TouristStructure'
        ], 'entity'),
        AttrRename(
            'locator',
            dict(Rooftop='rooftop',
                 Parcel='parcel',
                 ParcelCentroid='parcel',
                 Interpolation='interpolation',
                 InterpolationOffset='interpolation_offset')),
        AttrSorter(
            ['rooftop', 'parcel', 'interpolation_offset', 'interpolation'],
            'locator'),
        AttrSorter(['Address'], 'entity'),
        ScoreSorter(),
        GroupBy(('x', 'y')),
        GroupBy('match_addr')
    ]
    DEFAULT_POSTPROCESSORS = []

    def __init__(self, preprocessors=None, postprocessors=None, settings=None):
        preprocessors = Bing.DEFAULT_PREPROCESSORS if preprocessors is None else preprocessors
        postprocessors = Bing.DEFAULT_POSTPROCESSORS if postprocessors is None else postprocessors
        GeocodeService.__init__(self, preprocessors, postprocessors, settings)

    def _geocode(self, pq):
        if pq.query.strip() == '':
            # No single line query string; use address elements:
            query = {
                'addressLine': pq.address,
                'locality': pq.city,
                'adminDistrict': pq.state,
                'postalCode': pq.postal,
                'countryRegion': pq.country
            }
        else:
            query = {'query': pq.query}

        if pq.viewbox is not None:
            query = dict(query, **{'umv': pq.viewbox.to_bing_str()})
        if hasattr(pq, 'culture'):
            query = dict(query, c=pq.culture)
        if hasattr(pq, 'user_ip'):
            query = dict(query, uip=pq.user_ip)
        if hasattr(pq, 'user_lat') and hasattr(pq, 'user_lon'):
            query = dict(query, **{'ul': '%f,%f' % (pq.user_lat, pq.user_lon)})

        addl_settings = {'key': self._settings['api_key']}
        query = dict(query, **addl_settings)
        response_obj = self._get_json_obj(self._endpoint, query)
        returned_candidates = []  # this will be the list returned
        for r in response_obj['resourceSets'][0]['resources']:
            c = Candidate()
            c.entity = r['entityType']
            c.locator = r['geocodePoints'][0][
                'calculationMethod']  # ex. "Parcel"
            c.confidence = r['confidence']  # High|Medium|Low
            c.match_addr = r[
                'name']  # ex. "1 Microsoft Way, Redmond, WA 98052"
            c.x = r['geocodePoints'][0]['coordinates'][1]  # long, ex. -122.13
            c.y = r['geocodePoints'][0]['coordinates'][0]  # lat, ex. 47.64
            c.wkid = 4326
            c.geoservice = self.__class__.__name__
            returned_candidates.append(c)
        return returned_candidates
Ejemplo n.º 5
0
    def setUp(self):
        # Viewbox objects - callowhill is from BSS Spring Garden station to Wash. Sq.
        vb = {
            'callowhill': Viewbox(-75.162628, 39.962769, -75.150963, 39.956322)
        }
        # PlaceQuery objects
        self.pq = {  # North American Addresses:
            'azavea': PlaceQuery('340 N 12th St Ste 402 Philadelphia PA'),
            'ambiguous_azavea': PlaceQuery('340 12th St Ste 402 Philadelphia PA'),
            'zip_plus_4_in_postal_plus_country': PlaceQuery(postal='19127-1115', country='US'),
            'wolf': PlaceQuery('Wolf Building'),
            'wolf_philly': PlaceQuery('Wolf Building, Philadelphia PA'),
            'wolf_bounded': PlaceQuery('Wolf Building', bounded=True, viewbox=vb['callowhill']),
            'bounded_340_12th': PlaceQuery('340 12th St, Philadelphia PA',
                                           bounded=True, viewbox=vb['callowhill']),
            'alpha_774R_W_Central_Ave': PlaceQuery('774R W Central Ave Alpha NJ'),
            'alpha_774_W_Central_Ave_Rear': PlaceQuery('774 W Central Ave Rear, Alpha NJ'),
            '8_kirkbride': PlaceQuery('8 Kirkbride Rd 08822'),
            'george_washington': PlaceQuery('201 W Montmorency Blvd, George, Washington'),
            'pine_needles_dr': PlaceQuery('11761 pine needles providence forge'),
            'pine_needles_ct': PlaceQuery('5328 pine needles providence forge'),
            'pine_needles_terr': PlaceQuery('5359 pine needles providence forge'),
            'moorestown_hyphenated': PlaceQuery('111-113 W Main St Moorestown NJ'),
            'willow_street': PlaceQuery('2819F Willow Street Pike Willow Street PA'),
            'willow_street_parts': PlaceQuery(address='2819F Willow Street Pike',
                                              city='Willow Street', state='PA', country='US'),
            'quebec': PlaceQuery('756 Rue Berri Montreal QC', country='CA'),
            'quebec_accent': PlaceQuery('527 Ch. Beauséjour, Saint-Elzéar-de-Témiscouata QC'),
            'quebec_hyphenated': PlaceQuery('227-227A Rue Commerciale, Saint-Louis-du-Ha! Ha! QC'),
            'senado_mx': PlaceQuery('Paseo de la Reforma 135, Tabacalera, Cuauhtémoc, Distrito Federal, 06030'),
            'senado_mx_struct': PlaceQuery(address='Paseo de la Reforma 135', neighborhood='Tabacalera, Cuauhtémoc', subregion='', state='Distrito Federal', postal='06030', country='MX'),
            'robert_cheetham': PlaceQuery('Robert Cheetham, Philadelphia'),
            # European Addresses:
            'london_pieces': PlaceQuery(address='31 Maiden Lane', city='London', country='UK'),
            'london_one_line': PlaceQuery('31 Maiden Lane, London WC2E', country='UK'),
            'london_pieces_hyphenated': PlaceQuery(address='31-32 Maiden Lane', city='London',
                                                   country='UK'),
            'london_one_line_hyphenated': PlaceQuery('31-32 Maiden Lane London WC2E', country='UK'),
            # Oceanian Addresses:
            'karori': PlaceQuery('102 Karori Road Karori Wellington', country='NZ'),
        }

        if BING_MAPS_API_KEY is not None:
            bing_settings = dict(api_key=BING_MAPS_API_KEY)
            self.g_bing = Geocoder(
                [['omgeo.services.Bing', {
                    'settings': bing_settings
                }]])

        if MAPQUEST_API_KEY is not None:
            mapquest_settings = dict(api_key=MAPQUEST_API_KEY)
            self.g_mapquest = Geocoder(
                [['omgeo.services.MapQuest', {
                    'settings': mapquest_settings
                }]])
            self.g_mapquest_ssl = Geocoder([[
                'omgeo.services.MapQuestSSL', {
                    'settings': mapquest_settings
                }
            ]])

        if PELIAS_API_KEY is not None:
            pelias_settings = dict(api_key=PELIAS_API_KEY)
            self.g_pelias = Geocoder(
                [['omgeo.services.Pelias', {
                    'settings': pelias_settings
                }]])

        if GOOGLE_API_KEY is not None:
            self.g_google = Geocoder([[
                'omgeo.services.Google', {
                    'settings': {
                        'api_key': GOOGLE_API_KEY
                    }
                }
            ]])
            self.g_google_wo_postprocess = Geocoder([[
                'omgeo.services.Google', {
                    'settings': {
                        'api_key': GOOGLE_API_KEY
                    },
                    'postprocessors': []
                }
            ]])

        #: main geocoder used for tests, using default APIs
        self.g = Geocoder()

        # geocoders using individual services
        self.g_esri_wgs = Geocoder([['omgeo.services.EsriWGS', {}]])

        if ESRI_CLIENT_ID is not None and ESRI_CLIENT_SECRET is not None:
            self.g_esri_wgs_auth = Geocoder([[
                'omgeo.services.EsriWGS', {
                    'settings': {
                        'client_id': ESRI_CLIENT_ID,
                        'client_secret': ESRI_CLIENT_SECRET
                    }
                }
            ]])

        if MAPQUEST_API_KEY is not None:  # MapQuest's open Nominatime API now also requires a key
            self.g_nom = Geocoder([['omgeo.services.Nominatim', {}]])

        self.g_census = Geocoder([['omgeo.services.USCensus', {}]])

        ESRI_WGS_LOCATOR_MAP = {
            'PointAddress': 'rooftop',
            'StreetAddress': 'interpolation',
            'PostalExt': 'postal_specific',  # accept ZIP+4
            'Postal': 'postal'
        }
        ESRI_WGS_POSTPROCESSORS_POSTAL_OK = [
            AttrExclude(
                ['USA.Postal'], 'locator'
            ),  # accept postal from everywhere but US (need PostalExt)
            AttrFilter(
                ['PointAddress', 'StreetAddress', 'PostalExt', 'Postal'],
                'locator_type'),
            AttrSorter(
                ['PointAddress', 'StreetAddress', 'PostalExt', 'Postal'],
                'locator_type'),
            AttrRename('locator', ESRI_WGS_LOCATOR_MAP
                       ),  # after filter to avoid searching things we toss out
            UseHighScoreIfAtLeast(99.8),
            ScoreSorter(),
            GroupBy('match_addr'),
            GroupBy(('x', 'y')),
        ]
        GEOCODERS_POSTAL_OK = [[
            'omgeo.services.EsriWGS', {
                'postprocessors': ESRI_WGS_POSTPROCESSORS_POSTAL_OK
            }
        ]]
        self.g_esri_wgs_postal_ok = Geocoder(GEOCODERS_POSTAL_OK)

        #: geocoder with fast timeout
        self.impatient_geocoder = Geocoder(
            [['omgeo.services.EsriWGS', {
                'settings': {
                    'timeout': 0.001
                }
            }]])
Ejemplo n.º 6
0
class EsriWGS(GeocodeService):
    """
    Class to geocode using the `ESRI World Geocoding service
    <https://developers.arcgis.com/features/geocoding/>`_.

    This uses two endpoints -- one for single-line addresses,
    and one for multi-part addresses.

    An optional ``key`` parameter can be passed to the :class:`~omgeo.places.PlaceQuery`
    which will be passed as a ``magicKey`` to the find endpoint if
    using a single line address/text search. This allows omgeo
    to be used with the `Esri suggest endpoint
    <https://developers.arcgis.com/rest/geocode/api-reference/geocoding-suggest.htm>`_.

    .. warning::

        Based on tests using the magicKey parameter, it is
        recommended that a viewbox not be used with in conjuction
        with the magicKey. Additionally, address/search text passed
        via the query may be ignored when using a magicKey.

    An optional ``for_storage`` flag can be passed to the :class:`~omgeo.places.PlaceQuery` which
    will cause ``findStorage=true`` to be passed to the find endpoint.
    The Esri terms of service requires this if the returned point will be stored
    in a database.
    If you pass this flag, you `must` set the client_id and client_secret settings.

    Settings used by the EsriWGS GeocodeService object may include:
     * client_id --  The Client ID used to access Esri services.
     * client_secret --  The Client Secret used to access Esri services.

    """

    LOCATOR_MAP = {
        'PointAddress': 'rooftop',
        'StreetAddress': 'interpolation',
        'PostalExt': 'postal_specific',  # accept ZIP+4
        'Postal': 'postal'
    }

    DEFAULT_PREPROCESSORS = [CancelIfPOBox()]

    DEFAULT_POSTPROCESSORS = [
        AttrFilter(
            [
                'PointAddress',
                'StreetAddress',
                # 'PostalExt',
                # 'Postal'
            ],
            'locator_type'),
        # AttrExclude(['USA_Postal'], 'locator'), #accept postal from everywhere but US (need PostalExt)
        AttrSorter(
            [
                'PointAddress',
                'StreetAddress',
                # 'PostalExt',
                # 'Postal'
            ],
            'locator_type'),
        AttrRename(
            'locator',
            LOCATOR_MAP),  # after filter to avoid searching things we toss out
        UseHighScoreIfAtLeast(99.8),
        ScoreSorter(),
        GroupBy('match_addr'),
        GroupBy(('x', 'y')),
    ]

    _endpoint = 'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer'

    def __init__(self, preprocessors=None, postprocessors=None, settings=None):
        preprocessors = EsriWGS.DEFAULT_PREPROCESSORS if preprocessors is None else preprocessors
        postprocessors = EsriWGS.DEFAULT_POSTPROCESSORS if postprocessors is None else postprocessors
        settings = {} if settings is None else settings

        if 'client_id' in settings and 'client_secret' in settings:
            self._authenticated = True
            self._client_id = settings['client_id']
            self._client_secret = settings['client_secret']
            self._token = None
        elif 'client_id' not in settings and 'client_secret' not in settings:
            self._authenticated = False
        else:
            raise Exception(
                'Must specify both client_id and client_secret to use authentication'
            )

        GeocodeService.__init__(self, preprocessors, postprocessors, settings)

    def _geocode(self, pq):
        """
        :arg PlaceQuery pq: PlaceQuery object to use for geocoding
        :returns: list of location Candidates
        """
        #: List of desired output fields
        #: See `ESRI docs <https://developers.arcgis.com/rest/geocode/api-reference/geocoding-geocode-addresses.htm>_` for details
        outFields = (
            'Loc_name',
            # 'Shape',
            'Score',
            'Match_addr',  # based on address standards for the country
            # 'Address', # returned by default
            # 'Country' # 3-digit ISO 3166-1 code for a country. Example: Canada = "CAN"
            # 'Admin',
            # 'DepAdmin',
            # 'SubAdmin',
            # 'Locality',
            # 'Postal',
            # 'PostalExt',
            'Addr_type',
            # 'Type',
            # 'Rank',
            'AddNum',
            'StPreDir',
            'StPreType',
            'StName',
            'StType',
            'StDir',
            # 'Side',
            # 'AddNumFrom',
            # 'AddNumTo',
            # 'AddBldg',
            'City',
            'Subregion',
            'Region',
            'Postal',
            'Country',
            # 'Ymax',
            # 'Ymin',
            # 'Xmin',
            # 'Xmax',
            # 'X',
            # 'Y',
            'DisplayX',
            'DisplayY',
            # 'LangCode',
            # 'Status',
        )
        outFields = ','.join(outFields)
        query = dict(
            f='json',  # default HTML. Other options are JSON and KMZ.
            outFields=outFields,
            # outSR=WKID, defaults to 4326
            maxLocations=20,  # default 1; max is 20
        )

        # Postal-code only searches work in the single-line but not multipart geocoder
        # Remember that with the default postprocessors, postcode-level results will be eliminated
        if pq.query == pq.address == '' and pq.postal != '':
            pq.query = pq.postal

        if pq.query == '':  # multipart
            query = dict(
                query,
                Address=pq.
                address,  # commonly represents the house number and street name of a complete address
                Neighborhood=pq.neighborhood,
                City=pq.city,
                Subregion=pq.subregion,
                Region=pq.state,
                Postal=pq.postal,
                # PostalExt=
                CountryCode=pq.
                country,  # full country name or ISO 3166-1 2- or 3-digit country code
            )
        else:  # single-line
            magic_key = pq.key if hasattr(pq, 'key') else ''
            query = dict(
                query,
                singleLine=pq.
                query,  # This can be a street address, place name, postal code, or POI.
                sourceCountry=pq.
                country,  # full country name or ISO 3166-1 2- or 3-digit country code
            )
            if magic_key:
                query[
                    'magicKey'] = magic_key  # This is a lookup key returned from the suggest endpoint.

        if pq.bounded and pq.viewbox is not None:
            query = dict(query, searchExtent=pq.viewbox.to_esri_wgs_json())

        if self._authenticated:
            if self._token is None or self._token_expiration < datetime.utcnow(
            ):
                expiration = timedelta(hours=2)
                self._token = self.get_token(expiration)
                self._token_expiration = datetime.utcnow() + expiration

            query['token'] = self._token

        if getattr(pq, 'for_storage', False):
            query['forStorage'] = 'true'

        endpoint = self._endpoint + '/findAddressCandidates'
        response_obj = self._get_json_obj(endpoint, query)
        returned_candidates = []  # this will be the list returned
        try:
            locations = response_obj['candidates']
            for location in locations:
                c = Candidate()
                attributes = location['attributes']
                c.match_addr = attributes['Match_addr']
                c.locator = attributes['Loc_name']
                c.locator_type = attributes['Addr_type']
                c.score = attributes['Score']
                c.x = attributes[
                    'DisplayX']  # represents the actual location of the address.
                c.y = attributes['DisplayY']
                c.wkid = response_obj['spatialReference']['wkid']
                c.geoservice = self.__class__.__name__

                # Optional address component fields.
                for in_key, out_key in [('City', 'match_city'),
                                        ('Subregion', 'match_subregion'),
                                        ('Region', 'match_region'),
                                        ('Postal', 'match_postal'),
                                        ('Country', 'match_country')]:
                    setattr(c, out_key, attributes.get(in_key, ''))
                setattr(c, 'match_streetaddr',
                        self._street_addr_from_response(attributes))
                returned_candidates.append(c)
        except KeyError:
            pass
        return returned_candidates

    def _street_addr_from_response(self, attributes):
        """Construct a street address (no city, region, etc.) from a geocoder response.

        :param attributes: A dict of address attributes as returned by the Esri geocoder.
        """
        # The exact ordering of the address component fields that should be
        # used to reconstruct the full street address is not specified in the
        # Esri documentation, but the examples imply that it is this.
        ordered_fields = [
            'AddNum', 'StPreDir', 'StPreType', 'StName', 'StType', 'StDir'
        ]
        result = []
        for field in ordered_fields:
            result.append(attributes.get(field, ''))
        if any(result):
            return ' '.join([s for s in result
                             if s])  # Filter out empty strings.
        else:
            return ''

    def get_token(self, expires=None):
        """
        :param expires: The time until the returned token expires.
            Must be an instance of :class:`datetime.timedelta`.
            If not specified, the token will expire in 2 hours.
        :returns: A token suitable for use with the Esri geocoding API
        """
        endpoint = 'https://www.arcgis.com/sharing/rest/oauth2/token/'
        query = {
            'client_id': self._client_id,
            'client_secret': self._client_secret,
            'grant_type': 'client_credentials'
        }

        if expires is not None:
            if not isinstance(expires, timedelta):
                raise Exception(
                    'If expires is provided it must be a timedelta instance')
            query['expiration'] = int(expires.total_seconds() / 60)

        response_obj = self._get_json_obj(endpoint, query, is_post=True)

        return response_obj['access_token']