# Python

> Geocode with api.geolith.app from Python: requests for scripts, retries on 429, reverse geocoding, and a pandas batch.

Use a secret key (`gc_sk_…`) from an environment variable. Install `requests` (`pip install requests`).

## Search and reverse

```python
import os
import requests

API = "https://api.geolith.app"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['GEOLITH_API_KEY']}"


def search(text: str, country: str | None = None, size: int = 1) -> list[dict]:
    params = {"text": text, "size": size}
    if country:
        params["boundary.country"] = country
    res = session.get(f"{API}/v1/search", params=params, timeout=10)
    res.raise_for_status()
    return res.json()["features"]


def reverse(lat: float, lon: float) -> dict | None:
    res = session.get(
        f"{API}/v1/reverse",
        params={"point.lat": lat, "point.lon": lon, "size": 1},
        timeout=10,
    )
    res.raise_for_status()
    features = res.json()["features"]
    return features[0] if features else None


hit = search("eiffel tower", country="FR")[0]
lon, lat = hit["geometry"]["coordinates"]
print(hit["properties"]["label"], lat, lon)
print(reverse(18.922, 72.8347)["properties"]["label"])
```

## Retries

Retry `429 rate_limited` after `Retry-After`. Stop on `429 quota_exceeded`.

```python
import time


def get_with_retry(path: str, params: dict, tries: int = 5) -> dict:
    for attempt in range(tries):
        res = session.get(f"{API}{path}", params=params, timeout=10)
        if res.status_code == 429:
            if res.json().get("error", {}).get("code") == "quota_exceeded":
                res.raise_for_status()
            time.sleep(float(res.headers.get("Retry-After", "1")))
            continue
        if res.status_code >= 500:
            time.sleep(0.5 * 2**attempt)
            continue
        res.raise_for_status()
        return res.json()
    raise RuntimeError("geolith: gave up after retries")
```

## A batch with pandas

```python
import pandas as pd

df = pd.DataFrame({"address": ["Shaniwar Wada, Pune", "Gateway of India, Mumbai"]})


def first_point(address: str) -> pd.Series:
    features = get_with_retry("/v1/search", {"text": address, "size": 1})["features"]
    if not features:
        return pd.Series({"label": None, "lat": None, "lon": None})
    f = features[0]
    return pd.Series({
        "label": f["properties"]["label"],
        "lat": f["geometry"]["coordinates"][1],
        "lon": f["geometry"]["coordinates"][0],
    })


df = df.join(df["address"].apply(first_point))
print(df)
```

Results may be stored permanently. Keep the attribution, `© OpenStreetMap contributors (ODbL), Overture Maps Foundation`, with any data you show.
