Batch Geocoding a CSV of Canadian Addresses in Python
A production-ready Python script for geocoding large spreadsheets of Canadian civic addresses with automated retry logic, checkpoint recovery, and zero data leakage.
🛡 Privacy First: Zero Logging Guarantee
When batch-geocoding employee locations, patient registries, or customer files, Geo Service guarantees that none of your address strings, coordinates, or query parameters are ever written to server logs, databases, or third-party tracking services.
Prerequisites
Install the required standard data libraries:
pip install pandas requests tqdm
The Complete Python Geocoding Script
Save this script as geocode_batch.py. It loads your CSV, queries the Nominatim-compatible Canadian search endpoint, handles rate limits gracefully, and saves progress incrementally every 100 rows:
import time
import os
import pandas as pd
import requests
from tqdm import tqdm
API_KEY = os.environ.get("GEO_SERVICE_KEY", "YOUR_SERVER_API_KEY")
BASE_URL = "https://geo.jerielbenavides.com/search"
INPUT_CSV = "addresses.csv"
OUTPUT_CSV = "addresses_geocoded.csv"
def geocode_address(session, address_str):
"""Geocode a single Canadian address string using the Nominatim endpoint."""
headers = {
"Authorization": f"Bearer {API_KEY}",
"Accept": "application/json"
}
params = {
"q": address_str,
"format": "jsonv2",
"countrycodes": "ca",
"limit": 1
}
for attempt in range(4):
try:
resp = session.get(BASE_URL, headers=headers, params=params, timeout=10)
if resp.status_code == 200:
data = resp.json()
if data and len(data) > 0:
first = data[0]
return float(first["lat"]), float(first["lon"]), first.get("display_name", "")
return None, None, "NOT_FOUND"
elif resp.status_code == 429:
# Rate limit encountered: exponential backoff
wait_time = (2 ** attempt) + 0.5
time.sleep(wait_time)
else:
return None, None, f"HTTP_{resp.status_code}"
except requests.RequestException:
time.sleep(1)
return None, None, "ERROR_MAX_RETRIES"
def main():
print(f"Loading {INPUT_CSV}...")
df = pd.read_csv(INPUT_CSV)
if "latitude" not in df.columns:
df["latitude"] = None
df["longitude"] = None
df["matched_name"] = None
session = requests.Session()
total_rows = len(df)
print(f"Starting geocoding for {total_rows} Canadian addresses...")
for idx, row in tqdm(df.iterrows(), total=total_rows):
# Skip if already geocoded (checkpoint resume)
if pd.notna(row.get("latitude")):
continue
address = row.get("address", "")
lat, lon, matched = geocode_address(session, address)
df.at[idx, "latitude"] = lat
df.at[idx, "longitude"] = lon
df.at[idx, "matched_name"] = matched
# Periodic checkpoint save every 100 records
if idx % 100 == 0:
df.to_csv(OUTPUT_CSV, index=False)
df.to_csv(OUTPUT_CSV, index=False)
print(f"\nDone! Geocoded results saved to {OUTPUT_CSV}")
if __name__ == "__main__":
main()
Checkpoint Recovery
If your script is interrupted, re-running it automatically resumes from where it left off, avoiding wasted credits.
Exponential Backoff
Automatically handles HTTP 429 rate limit pauses dynamically without dropping rows or throwing unhandled errors.
Canadian Bounding
The countrycodes=ca parameter enforces strict Canadian territory matching across all provinces.
Want zero-code bulk processing?
If you prefer not to manage scripts, you can upload your CSV directly in our web console at Dashboard → Batch Lookups. Our cluster will process it asynchronously, encrypt the output with AES-256, and notify you when ready for download.
Start geocoding Canadian addresses
Get your server API key and run the script on your data in under 2 minutes.