def test_pro_filter_AttrFilter_inexact(self): """Test AttrFilter postprocessor with ``exact_match=False``.""" good_values = ['roof', 'parcel'] candidates_in = [self.best, self.good, self.better] candidates_exp = [self.best, self.better] # roof is a substr of rooftop candidates_out = AttrFilter(good_values, 'locator', exact_match=False).process(candidates_in) self.assertEqual_(candidates_out, candidates_exp)
def test_pro_filter_AttrFilter_exact(self): """Test AttrFilter postprocessor.""" good_values = ['roof', 'parcel'] candidates_in = [self.best, self.good, self.better] candidates_exp = [self.better] # just the one with the parcel locator candidates_out = AttrFilter(good_values, 'locator', exact_match=True).process(candidates_in) self.assertEqual_(candidates_out, candidates_exp)
class _EsriEUGeocodeService(): """ Base class including for Esri EU REST and SOAP Geocoders As of 29 Dec 2011, the ESRI website claims to support Andorra, Austria, Belgium, Denmark, Finland, France, Germany, Gibraltar, Ireland, Italy, Liechtenstein, Luxembourg, Monaco, The Netherlands, Norway, Portugal, San Marino, Spain, Sweden, Switzerland, United Kingdom, and Vatican City. """ _wkid = 4326 #: FIPS codes of supported countries SUPPORTED_COUNTRIES_FIPS = [ 'AN', 'AU', 'BE', 'DA', 'FI', 'FR', 'GM', 'GI', 'EI', 'IT', 'LS', 'LU', 'MN', 'NL', 'NO', 'PO', 'SM', 'SP', 'SW', 'SZ', 'UK', 'VT' ] #: ISO-2 codes of supported countries SUPPORTED_COUNTRIES_ISO2 = [ 'AD', 'AT', 'BE', 'DK', 'FI', 'FR', 'DE', 'GI', 'IE', 'IT', 'LI', 'LU', 'MC', 'NL', 'NO', 'PT', 'SM', 'ES', 'SE', 'CH', 'GB', 'VC' ] #: Map of FIPS to ISO-2 codes, if they are different. MAP_FIPS_TO_ISO2 = { 'AN': 'AD', 'AU': 'AT', 'DA': 'DK', 'GM': 'DE', 'EI': 'IE', 'LS': 'LI', 'MN': 'MC', 'PO': 'PT', 'SP': 'ES', 'SW': 'SE', 'SZ': 'CH', 'UK': 'GB', 'VT': 'VC', } #: Map to standardize locator LOCATOR_MAP = { 'EU_Street_Addr': 'interpolation', } DEFAULT_PREPROCESSORS = [ CountryPreProcessor(SUPPORTED_COUNTRIES_ISO2, MAP_FIPS_TO_ISO2), ParseSingleLine(), ] DEFAULT_POSTPROCESSORS = [ AttrFilter(['EU_Street_Addr'], 'locator', False), AttrRename('locator', LOCATOR_MAP), UseHighScoreIfAtLeast(100), GroupBy('match_addr'), ScoreSorter(), ]
def test_esri_geocoder_eu_default_override(self): """ Test for default argument bug in 3.1 -- EsriNA and EsriEU append processors rather than replace them """ geocoder = Geocoder([['omgeo.services.EsriEU', {'postprocessors': [AttrFilter([ 'rooftop', 'interpolation', 'postal_specific'], 'locator')]}]]) self.assertEqual(1, len(geocoder._sources[0]._postprocessors), 'EsriEU geocoder incorrectly processed defaults') self.assertEqual('AttrFilter', geocoder._sources[0]._postprocessors[0].__class__.__name__, 'EsriEU geocoder incorrectly processed defaults')
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(), ]
class Nominatim(GeocodeService): """ Class to geocode using `Nominatim services hosted by MapQuest <http://open.mapquestapi.com/nominatim/>`_. """ _wkid = 4326 _endpoint = 'http://open.mapquestapi.com/nominatim/v1/search' DEFAULT_ACCEPTED_ENTITIES = [ 'building.', 'historic.castle', 'leisure.ice_rink', 'leisure.miniature_golf', 'leisure.sports_centre', 'lesiure.stadium', 'leisure.track', 'lesiure.water_park', 'man_made.lighthouse', 'man_made.works', 'military.barracks', 'military.bunker', 'office.', 'place.house', 'amenity.', 'power.generator', 'railway.station', 'shop.', 'tourism.' ] DEFAULT_REJECTED_ENTITIES = [ 'amenity.drinking_water', 'amentity.bicycle_parking', 'amentity.ev_charging', 'amentity.grit_bin', 'amentity.atm', 'amentity.hunting_stand', 'amentity.post_box' ] DEFAULT_PREPROCESSORS = [ReplaceRangeWithNumber() ] # 766-68 Any St. -> 766 Any St. """Preprocessors to use with this geocoder service, in order of desired execution.""" DEFAULT_POSTPROCESSORS = [ AttrFilter(DEFAULT_ACCEPTED_ENTITIES, 'entity', exact_match=False), AttrExclude(DEFAULT_REJECTED_ENTITIES, 'entity') ] """Postprocessors to use with this geocoder service, in order of desired execution.""" def __init__(self, preprocessors=None, postprocessors=None, settings=None): preprocessors = Nominatim.DEFAULT_PREPROCESSORS if preprocessors is None else preprocessors postprocessors = Nominatim.DEFAULT_POSTPROCESSORS if postprocessors is None else postprocessors GeocodeService.__init__(self, preprocessors, postprocessors, settings) def _geocode(self, pq): query = { 'q': pq.query, 'countrycodes': pq.country, # only takes ISO-2 'format': 'json' } if pq.viewbox is not None: query = dict( query, **{ 'viewbox': pq.viewbox.to_mapquest_str(), 'bounded': pq.bounded }) response_obj = self._get_json_obj(self._endpoint, query) returned_candidates = [] # this will be the list returned for r in response_obj: c = Candidate() c.locator = 'parcel' # we don't have one but this is the closest match c.entity = '%s.%s' % (r['class'], r['type']) # ex.: "place.house" c.match_addr = r[ 'display_name'] # ex. "Wolf Building, 340, N 12th St, Philadelphia, Philadelphia County, Pennsylvania, 19107, United States of America" #TODO: shorten w/ pieces c.x = float(r['lon']) # long, ex. -122.13 # cast to float in 1.3.4 c.y = float(r['lat']) # lat, ex. 47.64 # cast to float in 1.3.4 c.wkid = self._wkid c.geoservice = self.__class__.__name__ returned_candidates.append(c) return returned_candidates
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)
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
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 } }]])
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']