Guide

Offline geocoding in GeoPandas

geopandas.tools.geocode takes any geopy-style geocoder. Give it GridPin and the addresses are geocoded on your machine: no API key, no rate limit, no request leaves it.

Install and download a country

terminalpip
$ pip install gridpin-mcp geopandas geopy   # gridpin-mcp includes the engine
$ gridpin-fetch us   # United States, 1.45 GB, verified download

Covered countries and file sizes: guides.

The geocoder class

GeoPandas calls the class it is given with your keyword arguments, then geocode or reverse for each row, and reads the answer as a geopy Location. From gridpin 0.7.0 the class ships in the wheel: pip install "gridpin[geopy]", then from gridpin.geopy import GridPin. With an older engine, save this as gridpin_geocoder.py.

gridpin_geocoder.pyPython
import gridpin
from geopy.location import Location
from geopy.point import Point
from gridpin_mcp import sheets


class GridPin:
    """A geopy-style geocoder over one local GridPin sheet: no API key, no network per query."""

    def __init__(self, country, sheet=None):
        self.engine = gridpin.Geocoder(str(sheet or sheets.sheet_path(country)))

    def geocode(self, query, exactly_one=True, limit=10):
        return self._places(self.engine.geocode(query, 1 if exactly_one else limit), exactly_one)

    def reverse(self, query, exactly_one=True, limit=10):
        point = Point(query)  # (lat, lon), "lat, lon" or a geopy Point
        hits = self.engine.reverse(point.latitude, point.longitude, 1 if exactly_one else limit)
        return self._places(hits, exactly_one)

    @staticmethod
    def _places(hits, exactly_one):
        places = [Location(_address(h), (h["lat"], h["lon"]), h) for h in hits]
        if not places:
            return None
        return places[0] if exactly_one else places


def _address(hit):
    street = " ".join(p for p in (hit.get("housenumber"), hit["street"]) if p)
    return ", ".join(p for p in (street, hit["commune"], hit.get("region"), hit["postcode"]) if p)

Geocode a column

geocode.pyPython
import geopandas
import pandas as pd
from shapely.geometry import Point
from gridpin_geocoder import GridPin

df = pd.DataFrame({"address": ["1600 Pennsylvania Ave NW, Washington, DC 20500",
                               "233 S Wacker Dr, Chicago, IL 60606",
                               "Nowhere Street 99999, Atlantis"]})
points = geopandas.tools.geocode(df.address, provider=GridPin, country="us")
places = geopandas.tools.reverse_geocode([Point(-77.0365, 38.8977)], provider=GridPin, country="us")
pointsoutput
>>> points
                     geometry                                                address
0  POINT (-77.03655 38.89768)  1600 PENNSYLVANIA Avenue Northwest, Washington, 20500
1  POINT (-87.63587 41.87866)                 233 South WACKER Drive, Chicago, 60606
2                 POINT EMPTY                                                    NaN
>>> places
                     geometry                                                address
0  POINT (-77.03655 38.89768)  1600 PENNSYLVANIA Avenue Northwest, Washington, 20500

An address that is not found becomes an empty point, not a guess. Each Location keeps the engine’s answer in .raw, including precision: house is a door-level match, street and city are approximate. GeoPandas waits between requests only for Nominatim, so nothing slows the loop down.

Without GeoPandas

geopy.pyPython
from gridpin_geocoder import GridPin

g = GridPin("us")
place = g.geocode("1600 Pennsylvania Ave NW, Washington, DC 20500")
place.address, place.latitude, place.longitude, place.raw["precision"]
g.reverse("38.8977, -77.0365")                      # (lat, lon), "lat, lon" or a geopy Point
g.geocode("Main St, Springfield", exactly_one=False, limit=5)

Large tables

geopandas.tools.geocode goes row by row on one core. For a large table, call the engine’s batch method, which spreads the rows over every core and is several times faster on a multi-core machine, and build the points yourself. Rows that are not found get missing coordinates:

batch.pyPython
import geopandas
import gridpin
from gridpin_mcp import sheets

g = gridpin.Geocoder(str(sheets.sheet_path("us")))   # df: the table from the example above
hits = [r[0] if r else None for r in g.geocode_many(df.address.tolist(), 1)]
gdf = geopandas.GeoDataFrame(
    df.assign(precision=[h and h["precision"] for h in hits]),
    geometry=geopandas.points_from_xy([h and h["lon"] for h in hits], [h and h["lat"] for h in hits]),
    crs="EPSG:4326")