# Agentic Geospatial Search Engine — API Reference

> **For AI agents:** This document is designed for machine consumption. It describes the full public API, authentication, async job pattern, request/response schemas, and error codes.

---

## Base URL

```
https://api.YOUR_DOMAIN
```

---

## Authentication

All public endpoints require a Bearer token in the `Authorization` header:

```
Authorization: Bearer <your_api_token>
```

Missing or invalid tokens return:

| HTTP status | Detail |
|---|---|
| `401` | `Authorization: Bearer <token> required` |
| `403` | `invalid token` |

`GET /health` does **not** require authentication.

---

## Async Job Pattern

All endpoints are **fire-and-forget**. Submitting a request returns a `job_id` immediately (HTTP 202). The client then polls `GET /jobs/{job_id}` until the result is ready.

### Job lifecycle

```
Client                          Server
  │                                │
  │── POST /forward ──────────────>│
  │<─ 202 { job_id, "queued" } ────│
  │                                │  (processing asynchronously)
  │── GET /jobs/{job_id} ─────────>│
  │<─ { status: "processing" } ────│
  │                                │  (result ready)
  │── GET /jobs/{job_id} ─────────>│
  │<─ { status: "complete",        │
  │     result: { ... } } ─────────│
```

### Polling strategy

- Wait **500 ms** after submit before first poll
- Poll every **1 s** thereafter
- Stop after **120 s** (service may be unavailable)

---

## Endpoints

---

### POST /forward

Semantic geospatial POI search. The primary search endpoint.

**Request body** (`application/json`):

```json
{
  "query": "wheelchair-accessible Italian restaurant",
  "lat": 52.5200,
  "lon": 13.4050,
  "radius_m": 1000,
  "limit": 10,
  "neighborhood": false,
  "weather": false,
  "map": false
}
```

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `query` | string | yes | — | Natural language search query |
| `lat` | float | yes | — | Latitude (-90 to 90) |
| `lon` | float | yes | — | Longitude (-180 to 180) |
| `radius_m` | int | no | 1000 | Search radius in metres |
| `limit` | int | no | 10 | Max results (1–50) |
| `neighborhood` | bool | no | false | Include neighbourhood POI profile |
| `weather` | bool | no | false | Include current weather |
| `map` | bool | no | false | Include static map image URL |

**Response** (`status: complete`):

```json
{
  "job_id": "3f8e2a1b-...",
  "status": "complete",
  "result": {
    "query": "wheelchair-accessible Italian restaurant",
    "location": { "lat": 52.52, "lon": 13.405, "display_name": "Berlin, Germany" },
    "results": [
      {
        "osm_id": 123456789,
        "osm_type": "node",
        "name": "Trattoria Roma",
        "description": "Italian restaurant. Wheelchair accessible. Outdoor seating.",
        "lat": 52.5198,
        "lon": 13.4048,
        "distance_m": 45,
        "relevance_score": 0.91,
        "combined_score": 0.87,
        "tags": { "amenity": "restaurant", "cuisine": "italian", "wheelchair": "yes" },
        "opening_hours": "Mo-Su 12:00-22:00",
        "is_open": true,
        "confidence": 0.95
      }
    ],
    "neighborhood": null,
    "weather": null,
    "suggested_queries": ["nearby parking", "wheelchair route to restaurant"]
  }
}
```

---

### POST /neighbourhood

Returns a POI category profile for a coordinate — useful for understanding what kind of area a location is in.

**Request body**:

```json
{
  "lat": 52.5200,
  "lon": 13.4050,
  "radius_m": 500
}
```

**Response result**:

```json
{
  "lat": 52.52,
  "lon": 13.405,
  "radius_m": 500,
  "categories": {
    "restaurant": 12,
    "cafe": 8,
    "transit_stop": 4,
    "park": 2,
    "pharmacy": 1
  },
  "walkability": "high",
  "summary": "Dense mixed-use urban area with strong food/drink and transit presence."
}
```

---

### POST /reverse

Full reverse geocoding: coordinates → address + optional context components.

**Request body**:

```json
{
  "lat": 52.5200,
  "lon": 13.4050,
  "neighborhood": false,
  "weather": false,
  "map": false
}
```

**Response result**:

```json
{
  "lat": 52.52,
  "lon": 13.405,
  "display_name": "Unter den Linden 1, Mitte, Berlin, 10117, Germany",
  "address": {
    "road": "Unter den Linden",
    "house_number": "1",
    "suburb": "Mitte",
    "city": "Berlin",
    "postcode": "10117",
    "country": "Germany",
    "country_code": "de"
  },
  "neighborhood": null,
  "weather": null
}
```

---

### POST /context

Fat context endpoint — returns address + timezone + weather + neighbourhood in a single parallel call. Designed for pre-loading LLM system prompts.

**Request body**:

```json
{
  "lat": 52.5200,
  "lon": 13.4050
}
```

**Response result**:

```json
{
  "lat": 52.52,
  "lon": 13.405,
  "address": { "display_name": "Mitte, Berlin, Germany", "..." : "..." },
  "timezone": { "iana": "Europe/Berlin", "utc_offset_h": 2, "dst": true },
  "weather": { "current": { "temp_c": 18.5, "condition": "Partly cloudy" }, "..." : "..." },
  "neighborhood": { "categories": { "..." : 0 }, "walkability": "high" }
}
```

---

### GET /weather

Current conditions and 7-day forecast.

**Query parameters**:

| Parameter | Type | Required | Description |
|---|---|---|---|
| `lat` | float | yes | Latitude |
| `lon` | float | yes | Longitude |

**Example**:

```
GET /weather?lat=52.52&lon=13.405
Authorization: Bearer <token>
```

**Response result**:

```json
{
  "lat": 52.52,
  "lon": 13.405,
  "timezone": "Europe/Berlin",
  "current": {
    "temp_c": 18.5,
    "feels_like_c": 17.2,
    "humidity_pct": 62,
    "wind_kph": 14,
    "condition": "Partly cloudy",
    "is_day": true
  },
  "forecast": [
    { "date": "2026-05-28", "max_c": 21, "min_c": 12, "condition": "Sunny", "precip_mm": 0 }
  ]
}
```

---

### GET /timezone

IANA timezone, UTC offset, and DST status for a coordinate.

**Query parameters**: `lat`, `lon`

**Response result**:

```json
{
  "lat": 52.52,
  "lon": 13.405,
  "iana": "Europe/Berlin",
  "utc_offset_h": 2,
  "dst": true
}
```

---

### GET /changes

Returns entities modified since a given timestamp within a bounding box. Useful for cache invalidation.

**Query parameters**:

| Parameter | Type | Required | Description |
|---|---|---|---|
| `since` | int | yes | Unix timestamp (seconds) |
| `min_lat` | float | yes | Bounding box south |
| `max_lat` | float | yes | Bounding box north |
| `min_lon` | float | yes | Bounding box west |
| `max_lon` | float | yes | Bounding box east |
| `limit` | int | no | Max results (default 100) |

**Response result**:

```json
{
  "count": 3,
  "entities": [
    { "osm_id": 123, "osm_type": "node", "last_modified_ts": 1748390400, "name": "Café Berlino" }
  ]
}
```

---

### GET /jobs/{job_id}

Poll for job status and result.

**Path parameter**: `job_id` — UUID returned by any submit endpoint.

**Response variants**:

```json
{ "job_id": "3f8e2a1b-...", "status": "queued" }
{ "job_id": "3f8e2a1b-...", "status": "processing" }
{ "job_id": "3f8e2a1b-...", "status": "complete", "result": { ... } }
{ "job_id": "3f8e2a1b-...", "status": "error", "error": "upstream error" }
```

| Status | Meaning |
|---|---|
| `queued` | Job accepted, awaiting processing |
| `processing` | Being processed by the API backend |
| `complete` | Result available in `result` field |
| `error` | Execution failed; see `error` field |

---

### GET /health

Liveness check. No authentication required.

```json
{ "status": "ok" }
```

---

## Rate Limits

Each API token is subject to a **sliding-window rate limit**:

| Limit | Value |
|---|---|
| Requests per token | 10 per 60 seconds |
| Window type | Sliding (per-second rolling) |

When the limit is exceeded the server returns:

```
HTTP 429 Too Many Requests
{"detail": "rate limit exceeded"}
```

If the global job queue is full (more than 500 pending jobs across all tokens), any new submission also returns:

```
HTTP 429 Too Many Requests
{"detail": "queue full, try again later"}
```

Retry after a short back-off (recommended: 1–5 seconds).

---

## Error Codes

| HTTP | Cause |
|---|---|
| `401` | Missing or malformed Authorization header |
| `403` | Invalid Bearer token |
| `404` | `GET /jobs/{job_id}` — job not found (expired or wrong ID) |
| `422` | Request body validation error (missing required fields) |
| `429` | Rate limit exceeded or queue full — back off and retry |
| `502` | Backend returned an error processing this job |
| `503` | Service temporarily unavailable |

---

## Recommended Client Implementation

```python
import httpx, time

BASE = "https://api.YOUR_DOMAIN"
TOKEN = "your_api_token"
HEADERS = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}

def search(query: str, lat: float, lon: float) -> dict:
    # Submit
    r = httpx.post(f"{BASE}/forward", json={"query": query, "lat": lat, "lon": lon}, headers=HEADERS)
    job_id = r.json()["job_id"]

    # Poll
    time.sleep(0.5)
    for _ in range(120):
        r = httpx.get(f"{BASE}/jobs/{job_id}", headers=HEADERS)
        data = r.json()
        if data["status"] == "complete":
            return data["result"]
        if data["status"] == "error":
            raise RuntimeError(data.get("error", "unknown error"))
        time.sleep(1.0)
    raise TimeoutError("job did not complete within 120 s")
```

---

## MCP Tool Example

```python
# MCP tool definition wrapping /context for LLM pre-loading
{
  "name": "get_location_context",
  "description": "Get rich geospatial context for a coordinate: address, timezone, weather, POI profile.",
  "input_schema": {
    "type": "object",
    "properties": {
      "lat": {"type": "number"},
      "lon": {"type": "number"}
    },
    "required": ["lat", "lon"]
  }
}
# Handler calls POST /context, polls /jobs/{id}, returns result as tool output
```

---

## Pricing

| Tier | Price | Included requests | Overage |
|---|---|---|---|
| Free | $0 / mo | 3,000 | — |
| Starter | $49 / mo | 10,000 | $7.00 / 1,000 |
| Growth | $299 / mo | 100,000 | $3.08 / 1,000 |
| Pro | $999 / mo | 600,000 | $1.67 / 1,000 |
| Enterprise | Custom | Custom volume + SLA | — |

The free tier requires no credit card. Overage is billed at the per-request rate implied by the plan price after subtracting the 3,000 free requests included in all tiers.

---

## Data Sources

| Component | Source | License |
|---|---|---|
| POI data | OpenStreetMap | ODbL |
| Weather | Open-Meteo | CC BY 4.0 |
| Timezone | IANA timezone database | Public domain |
