Guide

geopy: GeocoderInsufficientPrivileges, GeocoderRateLimited and ConfigurationError

What each geopy exception means, why the Nominatim geocoder refuses some user agents, why a geocoded column fills with None, and settings for RateLimiter that keep a script within the limits. Checked with geopy 2.5.0.

From HTTP status to exception

geopy turns every answer other than 200 into an exception. All of them derive from GeocoderServiceError:

HTTP statusExceptionUsually
400, 412, 413, 414GeocoderQueryErrorA wrong parameter, or a query that is too long.
401, 407GeocoderAuthenticationFailureA missing or wrong API key.
402GeocoderQuotaExceededThe paid allowance is used up.
403GeocoderInsufficientPrivilegesBlocked by the public Nominatim, or a key not allowed for this request.
408, 503, 504GeocoderTimedOutThe service is overloaded or did not answer in time.
429GeocoderRateLimitedToo many requests; a subclass of GeocoderQuotaExceeded.
502GeocoderServiceErrorThe service failed.
no answer in timeGeocoderTimedOutThe default timeout is 1 second; give timeout=10 to the geocoder.

The message of a status error reads like Non-successful status code 403.

ConfigurationError: “Using Nominatim with default or sample user_agent”

For the public server nominatim.openstreetmap.org, geopy refuses the default user agent (geopy/2.5.0) and the samples from its documentation (my-application, my_app/1, my_user_agent/1.0, specify_your_app_name_here). It raises ConfigurationError before sending anything, because such requests break the usage policy and end in 403 or 429. Name your own app:

from geopy.geocoders import Nominatim

geolocator = Nominatim(user_agent="shop-locator/1.2 (admin@example.com)", timeout=10)
print(geolocator.geocode("Bundesplatz 3, Bern"))

The check applies only to the public server. With another domain, geopy sends what you set.

GeocoderInsufficientPrivileges: Non-successful status code 403

With the public Nominatim this means the server blocked the client: no proper user agent, more than one request a second, the same queries again and again, or autocomplete. The details are in Nominatim: “Access blocked”, 403 and 429. Other services answer 403 for a key that is not allowed from this place, such as a Google key restricted to websites used from a server.

A script that works on your computer and gets 403 on a cloud host (Streamlit Cloud, Colab, a shared CI runner) may share its IP address with other clients that were blocked.

GeocoderRateLimited: 429

The service wants fewer requests. Its Retry-After header, when sent, is in e.retry_after. Wait at least that long, then go on more slowly.

RateLimiter, and why every row is None

RateLimiter spaces the calls. Its defaults (geopy 2.5.0) are min_delay_seconds=0.0, max_retries=2, error_wait_seconds=5.0, swallow_exceptions=True and return_value_on_exception=None:

  • With the default delay of 0 it does not slow anything down. Set min_delay_seconds=1 for the public Nominatim.
  • Every GeocoderServiceError is retried, so a 403 block is sent again twice, 5 seconds apart.
  • After the last try it returns None and writes a warning to the log. A blocked run therefore fills the whole column with None. Set swallow_exceptions=False while testing, so that the first error stops the run.
import pandas as pd
from geopy.extra.rate_limiter import RateLimiter

geocode = RateLimiter(geolocator.geocode, min_delay_seconds=1, swallow_exceptions=False)
df = pd.DataFrame({"address": ["Bundesplatz 3, Bern", "Rue du Rhône 42, Genève"]})
unique = df["address"].drop_duplicates()             # each address once
found = {a: geocode(a) for a in unique}
df["lat"] = df["address"].map(lambda a: found[a].latitude if found[a] else None)
df["lon"] = df["address"].map(lambda a: found[a].longitude if found[a] else None)

At one request a second, 10,000 addresses take almost three hours, and the usage policy discourages bulk geocoding of larger amounts on the public server.

None without an error

geocode() returns None when the service found nothing. Services on OpenStreetMap data know only the house numbers mapped there; try the street and town alone, check for a stray country or postcode, and leave out apartment numbers and c/o lines.

Use another server

The Nominatim and Photon geocoders of geopy take a domain. With overpass.ch the key goes into it, and the answers keep their format:

from geopy.geocoders import Nominatim, Photon

KEY = "YOUR_KEY"
nominatim = Nominatim(domain=f"api.overpass.ch/v1/{KEY}/nominatim", user_agent="shop-locator/1.2", timeout=10)
photon = Photon(domain=f"api.overpass.ch/v1/{KEY}/geocode", user_agent="shop-locator/1.2", timeout=10)
print(nominatim.geocode("Bundesplatz 3, Bern", addressdetails=True).raw["address"])
print(photon.reverse("47.3769, 8.5417"))

Errors from overpass.ch become the same exceptions: 401 GeocoderAuthenticationFailure, 403 GeocoderInsufficientPrivileges, 429 GeocoderRateLimited. The plans allow far more than one request a second; see pricing.

Geocoding from overpass.ch

The Nominatim and Photon APIs on OpenStreetMap data, tested with geopy every night. 100'000 requests a month are free, and you may store the results.