Geo Service
← All Guides & Resources • Python Tutorial

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:

geocode_batch.py Python 3
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.