-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4 from bushwickayudamutua/add/google-maps-api
Add address normalization automation
- Loading branch information
Showing
13 changed files
with
278 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import googlemaps | ||
|
||
from bam_core.settings import GOOGLE_MAPS_API_KEY | ||
from bam_core.constants import MAYDAY_LOCATION, MAYDAY_RADIUS | ||
|
||
|
||
class GoogleMaps(object): | ||
def __init__(self, api_key=GOOGLE_MAPS_API_KEY): | ||
self.api_key = api_key | ||
|
||
@property | ||
def client(self): | ||
return googlemaps.Client(key=self.api_key) | ||
|
||
def get_place( | ||
self, | ||
address, | ||
location=MAYDAY_LOCATION, | ||
radius=MAYDAY_RADIUS, | ||
types=["premise", "subpremise", "geocode"], | ||
language="en-US", | ||
strict_bounds=True, | ||
): | ||
""" | ||
Get a place from the Google Maps API | ||
Args: | ||
address (str): The address to search for | ||
location (tuple): The location to search around | ||
radius (int): The radius to search within | ||
types (list): The types of places to search for | ||
language (str): The language to search in | ||
strict_bounds (bool): Whether to use strict bounds | ||
""" | ||
return self.client.places_autocomplete( | ||
address, | ||
location=location, | ||
radius=radius, | ||
types=types, | ||
language=language, | ||
strict_bounds=strict_bounds, | ||
) | ||
|
||
def get_normalized_address(self, address): | ||
""" | ||
Normalize an address using the Google Maps API | ||
Args: | ||
address (str): The address to normalize | ||
""" | ||
return self.client.addressvalidation(address) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
from typing import Any, Dict | ||
import requests | ||
|
||
|
||
class NycPlanningLabs(object): | ||
base_url = "https://geosearch.planninglabs.nyc/v2" | ||
|
||
def __init__(self): | ||
self.session = requests.Session() | ||
self.session.headers.update( | ||
{"Content-Type": "application/json", "Accept": "application/json"} | ||
) | ||
|
||
def search(self, text: str, size: int = 1) -> Dict[str, Any]: | ||
""" | ||
Search for a location in NYC using the geosearch API | ||
Args: | ||
text (str): The text to search for | ||
size (int): The number of results to return | ||
Returns: | ||
Dict[str, Any]: The response from the geosearch API | ||
""" | ||
url = f"{self.base_url}/search" | ||
params = {"text": text, "size": size} | ||
response = self.session.get(url, params=params) | ||
response.raise_for_status | ||
return response.json() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
from typing import Dict, Optional | ||
from bam_core.lib.google import GoogleMaps | ||
from bam_core.lib.nyc_planning_labs import NycPlanningLabs | ||
|
||
COMMON_ZIPCODE_MISTAKES = { | ||
"112007": "11207", | ||
} | ||
|
||
|
||
def _fix_zip_code(zip_code: Optional[str]) -> str: | ||
""" | ||
Attempt to fix common mistakes in zipcodes | ||
""" | ||
return COMMON_ZIPCODE_MISTAKES.get(zip_code, zip_code) | ||
|
||
|
||
def format_address( | ||
address: Optional[str] = None, | ||
city_state: Optional[str] = "", | ||
zipcode: Optional[str] = "", | ||
) -> Dict[str, str]: | ||
""" | ||
Format an address using the Google Maps API and the NYC Planning Labs API | ||
Args: | ||
address (str): The address to format | ||
city_state (str): The city and state to use if the address is missing | ||
zipcode (str): The zipcode to use if the address is missing | ||
Returns: | ||
Dict[str, str]: The formatted address, bin, and accuracy | ||
""" | ||
# connect to APIs | ||
gmaps = GoogleMaps() | ||
nycpl = NycPlanningLabs() | ||
|
||
response = { | ||
"cleaned_address": "", | ||
"bin": "", | ||
"cleaned_address_accuracy": "No result", | ||
} | ||
# don't do anything for missing addresses | ||
if not address or not address.strip(): | ||
return response | ||
|
||
# format address for query | ||
address_query = f"{address.strip()} {city_state.strip() or 'New York'} {_fix_zip_code(zipcode.strip())}".strip().upper() | ||
|
||
# lookup address using Google Maps Places API | ||
place_response = gmaps.get_place(address_query) | ||
if not len(place_response): | ||
return response | ||
|
||
place_address = place_response[0]["description"] | ||
if "subpremise" in place_response[0]["types"]: | ||
response["cleaned_address_accuracy"] = "Apartment" | ||
elif "premise" in place_response[0]["types"]: | ||
response["cleaned_address_accuracy"] = "Building" | ||
else: | ||
# ignore geocode results if not at the level of a building or apartment | ||
return response | ||
|
||
# lookup the cleaned address using the google maps address validation api | ||
norm_address_result = gmaps.get_normalized_address(place_address) | ||
|
||
norm_address = norm_address_result.get("result", {}) | ||
## TODO: Figure out if we should report granularity from here or places API | ||
# granularity = norm_address.get("verdict", {}).get("validationGranularity", "") | ||
# if granularity == "SUB_PREMISE": | ||
# response["cleaned_address_accuracy"] = "Apartment" | ||
# elif granularity == "PREMISE": | ||
# response["cleaned_address_accuracy"] = "Building" | ||
|
||
usps_data = norm_address.get("uspsData", {}).get("standardizedAddress", {}) | ||
cleaned_address = ( | ||
usps_data.get("firstAddressLine", "") | ||
+ " " | ||
+ usps_data.get("cityStateZipAddressLine", "") | ||
).strip() | ||
if not cleaned_address: | ||
# if no USPS data, use the formatted address | ||
cleaned_address = ( | ||
norm_address.get("address", {}).get("formattedAddress", "").upper() | ||
) | ||
# if no formatted address, use the place address | ||
if not cleaned_address: | ||
cleaned_address = place_address.upper() | ||
response["cleaned_address"] = cleaned_address | ||
|
||
# lookup the bin using the nyc planning labs api | ||
nycpl_response = nycpl.search(cleaned_address) | ||
response["bin"] = ( | ||
nycpl_response.get("features", [{}])[0] | ||
.get("properties", {}) | ||
.get("addendum", {}) | ||
.get("pad", {}) | ||
.get("bin", "") | ||
) | ||
return response |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.