Source code for operators.google_geocode_batch

"""This operator performs batch geocoding using the Google Place API's findplacefromtext endpoint."""

import requests
import pandas as pd
from airflow.models import BaseOperator
from airflow.utils.decorators import apply_defaults


[docs] class BatchGooglePlaceFindPlaceOperator(BaseOperator): """ Airflow Operator to perform batch geocoding using the Google Place API's findplacefromtext endpoint. This operator expects a pandas DataFrame with the columns "place_name" and "address". For each row, it will query the API by combining these two columns and append the following columns to the DataFrame: - formatted_address - latitude - longitude :param input_df: The input pandas DataFrame with columns "place_name" and "address". :type input_df: pandas.DataFrame :param api_key: Your Google API key. :type api_key: str :param language: (optional) The language for the request. Default is 'en'. :type language: str :param fields: (optional) A comma-separated list of fields to return (e.g., "formatted_address,name,geometry"). :type fields: str :return: The input DataFrame with additional columns for formatted_address, latitude, and longitude. """ @apply_defaults def __init__( self, input_df: pd.DataFrame, api_key: str, language: str = "en", fields: str = "formatted_address,name,geometry", *args, **kwargs, ): super().__init__(*args, **kwargs) self.input_df = input_df self.api_key = api_key self.language = language self.fields = fields
[docs] def execute(self, context): endpoint = "https://maps.googleapis.com/maps/api/place/findplacefromtext/json" # Create a copy of the dataframe to avoid modifying the original df = self.input_df.copy() # Initialize new columns with default values df["formatted_address"] = None df["latitude"] = None df["longitude"] = None for idx, row in df.iterrows(): # Combine the place_name and address for a more robust query input_text = f"{row['place_name']}, {row['address']}" params = { "input": input_text, "inputtype": "textquery", "language": self.language, "key": self.api_key, } if self.fields: params["fields"] = self.fields self.log.info("Requesting place search for input: %s", input_text) try: response = requests.get(endpoint, params=params) response.raise_for_status() except Exception as e: self.log.error("Request failed for input '%s': %s", input_text, str(e)) continue data = response.json() if data.get("status") != "OK": error_msg = data.get("error_message", "Unknown error") self.log.error( "Place API error for input '%s': %s", input_text, error_msg ) continue candidates = data.get("candidates", []) if not candidates: self.log.warning("No candidates found for input: %s", input_text) continue candidate = candidates[0] # Use the first candidate returned self.log.info("Found candidate for input '%s': %s", input_text, candidate) # Extract required information formatted_address = candidate.get("formatted_address") geometry = candidate.get("geometry", {}) location = geometry.get("location", {}) latitude = location.get("lat") longitude = location.get("lng") # Update the DataFrame df.at[idx, "formatted_address"] = formatted_address df.at[idx, "latitude"] = latitude df.at[idx, "longitude"] = longitude # Optionally, the result can be pushed via XCom return df