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
$ 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.
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
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")
>>> 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
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:
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")