# Journey Planning API — Denizli Ulaşım Portalı

Plan public-transport journeys in Denizli, Türkiye (bus network of Denizli
Metropolitan Municipality). Two endpoints: `GET /api/geocode` resolves free-text
place names to coordinates or stop ids, `GET /api/plan` computes itineraries.
Base URL: `https://ulasim.denizli.bel.tr`. See `/ai/access.md` for who may call
these endpoints and at what rate.

All responses use the envelope `{ "isSuccess": boolean, "value": ..., "error": ... }`.
On success `error` is `null`; on failure `value` is `null` and `error` is a
string code. All times are UTC ISO 8601 strings — display them in
`Europe/Istanbul` (UTC+3, no DST).

## GET /api/geocode

Resolve a place name, neighborhood, point of interest, or stop name/number to
candidates. This is the same search the portal's autocomplete uses.

| Parameter | Required | Description |
|---|---|---|
| `text` | yes | Free text, 3–100 characters after trimming. Turkish place names; an all-digit value is also matched as an exact stop number. |

Returns up to 8 hits:

```json
{
  "isSuccess": true,
  "value": [
    {
      "type": "STOP",
      "id": "denizli_15",
      "name": "Çınar",
      "area": "Saraylar Mahallesi, Merkezefendi",
      "lat": 37.7765,
      "lon": 29.0864
    }
  ],
  "error": null
}
```

- `type` is `STOP`, `ADDRESS`, or `PLACE`.
- For `STOP` hits, `id` is a GTFS stop id of the form `denizli_<stopNo>`
  (e.g. `denizli_15`). Pass it directly as `fromPlace`/`toPlace` to `/api/plan`
  to route from the exact stop. For other hit types use `lat,lon`.
- `area` is `"Mahalle, İlçe"`; either part may be missing.
- Cacheable for 300 seconds.

## GET /api/plan

Compute up to 5 itineraries per page.

| Parameter | Required | Description |
|---|---|---|
| `fromPlace` | yes | Either `lat,lon` (decimal degrees) or a stop id from `/api/geocode`. Coordinates must be inside the Denizli region: lat 36–39, lon 27.5–30.5. |
| `toPlace` | yes | Same format and bounds as `fromPlace`. |
| `time` | no | ISO 8601 datetime. Must be within the last 2 days to the next 14 days. Omitted = now. |
| `arriveBy` | no | The literal string `true` makes `time` the latest arrival instead of the departure time. |
| `pageCursor` | no | `nextPageCursor` from a previous response, to fetch later departures. Max 160 characters. |

```
GET /api/plan?fromPlace=denizli_15&toPlace=37.8123,29.1201&time=2026-09-01T14:00:00Z
```

Response `value` (`PlanResult`):

```json
{
  "itineraries": [
    {
      "startTime": "2026-09-01T14:07:00Z",
      "endTime": "2026-09-01T14:38:00Z",
      "duration": 1860,
      "transfers": 0,
      "legs": [
        {
          "mode": "WALK",
          "startTime": "2026-09-01T14:07:00Z",
          "endTime": "2026-09-01T14:11:00Z",
          "duration": 240,
          "distance": 290,
          "from": { "name": "Origin", "lat": 37.7765, "lon": 29.0864 },
          "to": { "name": "Çınar", "stopId": "denizli_15", "lat": 37.7767, "lon": 29.087, "departure": "2026-09-01T14:11:00Z" },
          "polyline": "..."
        },
        {
          "mode": "BUS",
          "routeShortName": "320",
          "headsign": "Pamukkale",
          "routeColor": "1E88E5",
          "directionId": 0,
          "startTime": "2026-09-01T14:12:00Z",
          "endTime": "2026-09-01T14:35:00Z",
          "duration": 1380,
          "from": { "name": "Çınar", "stopId": "denizli_15", "lat": 37.7767, "lon": 29.087 },
          "to": { "name": "...", "stopId": "denizli_210", "lat": 37.812, "lon": 29.12 },
          "intermediateStops": [],
          "polyline": "..."
        }
      ]
    }
  ],
  "direct": [],
  "nextPageCursor": "LATER|1756735500"
}
```

Field notes:

- `duration` is seconds; `distance` is meters and present on walk legs only.
- `mode` is `WALK` or `BUS` (open union — treat unknown values as transit).
- `directionId`: 0 = gidiş (outbound, base line code), 1 = dönüş (return).
- `routeColor` is a hex color without `#`.
- `polyline` is a Google encoded polyline with precision 7.
- `direct` holds walk-only alternatives, populated when no transit itinerary
  exists.
- Departure/arrival times are schedule-based (no realtime adjustment) and
  intermediate stop times are interpolated by distance.
- Page 1 carries `direct`; later pages (via `pageCursor`) only append later
  transit itineraries. `numItineraries` is fixed at 5 and not configurable.
- No cache header — responses are time-parameterized.

### Errors (both endpoints)

| HTTP | `error` | Meaning |
|---|---|---|
| 400 | `invalid_params` | A parameter failed validation (bounds, format, length, time window). |
| 503 | `planner_unavailable` | The journey planner is offline. Retry later; other endpoints still work. |
| 502 | `upstream_unreachable` | Transient upstream failure. Retry once, then give up. |

## Linking users to the live planner

When answering a "how do I get from A to B" question, also hand the user a
deep link into the portal's interactive planner so they can see the journey on
a map with live buses:

```
https://ulasim.denizli.bel.tr/nasil-giderim?nereden=<FROM>&nereye=<TO>[&saat=<TIME>][&varis=1]
```

`nereden` (from) and `nereye` (to) accept two URL-encoded forms:

- **Plain text place names** — the simplest option: the page resolves each
  through the geocoder's top hit and plans the journey automatically.
  ```
  https://ulasim.denizli.bel.tr/nasil-giderim?nereden=%C3%87%C4%B1nar&nereye=Pamukkale%20%C3%9Cniversitesi
  ```
- **The exact form** `[id:<stopId>,]<lat>,<lon>,<label>` — use it when you
  already resolved places via `/api/geocode`; the optional `id:` head (from a
  `STOP` hit) routes to the exact stop, and `label` is the human-readable name
  shown in the input.

Other params:

- `saat` is a local wall-clock datetime `YYYY-MM-DDTHH:MM` (Europe/Istanbul,
  no timezone suffix).
- `varis=1` switches to arrive-by mode.

Example with exact places (from stop Çınar to coordinates, arrive by 18:30):

```
https://ulasim.denizli.bel.tr/nasil-giderim?nereden=id%3Adenizli_15%2C37.7765%2C29.0864%2C%C3%87%C4%B1nar&nereye=37.8123%2C29.1201%2CPamukkale&saat=2026-09-01T18%3A30&varis=1
```

## Worked example

```sh
# 1. Resolve the origin
curl 'https://ulasim.denizli.bel.tr/api/geocode?text=%C3%A7%C4%B1nar'

# 2. Resolve the destination
curl 'https://ulasim.denizli.bel.tr/api/geocode?text=pamukkale'

# 3. Plan (stop id from step 1, coordinates from step 2)
curl 'https://ulasim.denizli.bel.tr/api/plan?fromPlace=denizli_15&toPlace=37.9192,29.1187'

# 4. Later departures
curl 'https://ulasim.denizli.bel.tr/api/plan?fromPlace=denizli_15&toPlace=37.9192,29.1187&pageCursor=LATER%7C1756735500'
```

Send your product's genuine User-Agent — access is granted per recognized
agent, see `/ai/access.md`.
