FreeAPI.watch

Geocoding with Nominatim: Why You Must Set a User-Agent

Languages: curl, Python, JavaScript  ·  Estimated time: ~6 minutes  ·  Nominatim (OpenStreetMap) live status ↗

The missing User-Agent header breaks more Nominatim integrations than anything else. This tutorial covers the correct setup, rate limiting, batching strategies, and when to fall back to OpenCage.

Nominatim's User-Agent requirement is the number one reason integrations fail. Requests without a User-Agent return HTTP 403 or get silently dropped. Set it correctly and Nominatim is one of the most reliable free APIs we monitor.

Live status at /nominatim.

The Correct User-Agent Format

The OSM Usage Policy requires a User-Agent that identifies your application and a way to contact you. The format is informal — any non-empty string that identifies your app is technically acceptable. In practice:

# Wrong — will be blocked
curl "https://nominatim.openstreetmap.org/search?q=London&format=json"

# Correct — identify your app and contact
curl -H "User-Agent: MyWeatherApp/1.0 (contact@example.com)" \
  "https://nominatim.openstreetmap.org/search?q=London&format=json&limit=1"

# Response:
# [{"lat":"51.5074456","lon":"-0.1277653","display_name":"London, Greater London, England, United Kingdom",...}]

Python: Forward Geocoding with Rate Limit

import requests
import time
from functools import lru_cache

NOMINATIM_BASE = "https://nominatim.openstreetmap.org"
HEADERS = {"User-Agent": "MyApp/1.0 (contact@yourdomain.com)"}

@lru_cache(maxsize=1000)
def geocode(query: str) -> dict | None:
    """
    Forward geocode a query string. Results cached in memory.
    """
    resp = requests.get(
        f"{NOMINATIM_BASE}/search",
        params={"q": query, "format": "json", "limit": 1},
        headers=HEADERS,
        timeout=10
    )
    resp.raise_for_status()
    results = resp.json()
    if not results:
        return None
    r = results[0]
    return {
        "lat": float(r["lat"]),
        "lon": float(r["lon"]),
        "display_name": r["display_name"]
    }

def reverse_geocode(lat: float, lon: float) -> dict | None:
    """
    Reverse geocode coordinates to address.
    """
    resp = requests.get(
        f"{NOMINATIM_BASE}/reverse",
        params={"lat": lat, "lon": lon, "format": "json"},
        headers=HEADERS,
        timeout=10
    )
    resp.raise_for_status()
    return resp.json()

# Geocode multiple locations with rate limiting
locations = ["Paris, France", "Berlin, Germany", "Tokyo, Japan"]
results = []
for loc in locations:
    result = geocode(loc)
    if result:
        results.append(result)
    time.sleep(1)  # Respect 1 req/sec limit

JavaScript: Geocoding in the Browser

For browser-side geocoding, note that the same User-Agent requirement applies — but browsers don't let you set the User-Agent header (it's a forbidden header). Nominatim generally works from browsers anyway because the browser sets its own User-Agent. For server-side Node.js, you must set it explicitly.

// Node.js server-side (must set User-Agent)
async function geocode(query) {
  const url = new URL("https://nominatim.openstreetmap.org/search");
  url.searchParams.set("q", query);
  url.searchParams.set("format", "json");
  url.searchParams.set("limit", "1");

  const res = await fetch(url, {
    headers: { "User-Agent": "MyApp/1.0 (contact@yourdomain.com)" }
  });
  const results = await res.json();
  return results[0] ?? null;
}

// Simple rate limiter
let lastCall = 0;
async function rateLimitedGeocode(query) {
  const now = Date.now();
  const elapsed = now - lastCall;
  if (elapsed < 1000) {
    await new Promise(r => setTimeout(r, 1000 - elapsed));
  }
  lastCall = Date.now();
  return geocode(query);
}

When Nominatim Isn't Enough

Nominatim struggles with: partial addresses, business name searches, non-Latin scripts (quality varies), and autocomplete (it's designed for complete queries). If you're hitting these walls:

OpenCage (2,500 calls/day free, key required) aggregates multiple geocoding backends including OSM, and handles these edge cases better. See /opencage for current status and our comparison of geocoding APIs at /category/geocoding.

Frequently Asked Questions

Why does Nominatim need a User-Agent?

The OpenStreetMap Foundation runs Nominatim as a free public service. The User-Agent requirement lets their operations team identify and contact developers who are abusing the service (accidentally or intentionally). It's a courtesy requirement, not a technical one — but missing it results in your requests being blocked.

How do I run my own Nominatim instance?

Nominatim is open-source (nominatim.org). You can run it on a machine with the OpenStreetMap planet dump loaded (~1TB disk space). The OSM wiki has setup guides. Running your own instance removes all rate limits and lets you use it for high-volume commercial geocoding without paying for a commercial API.

Is OpenCage worth the cost for higher volume?

OpenCage's free tier gives 2,500 calls/day — 2.5x Nominatim's comfortable rate. Their $50/month plan gives 10,000 calls/day. They also aggregate multiple geocoding backends (not just OSM), which improves quality for hard addresses. If you're hitting Nominatim's limits, OpenCage is the natural next step. See /opencage for live status.

← All tutorials  ·  Check Nominatim (OpenStreetMap) live status →