# 0025. Distance ordering reads the visitor's position from a browser cookie

- **Status:** Accepted
- **Date:** 2026-08-08

## Context

The storage list had to gain a "distance from me" ordering, with the explicit requirement that it stay fast.
The visitor's position is only knowable in the browser, while the ordering has to happen in SQL, next to the
paging.

`resources/js/components/Geo.js` already resolved a position for the "nearest location" button — browser
geolocation, falling back to a geocoded address — and stored it in a plain `r24-geo=lat,lng` cookie for an
hour.

## Decision

The server reads that same cookie. [`helpers/Geo.php`](../../helpers/Geo.php) parses it and carries the
matching haversine formula; `LocationService::distancesFrom()` measures every published location in PHP —
there are a couple of dozen — and `StorageSearch` hands SQL the result as a lookup:

```sql
ORDER BY CASE `locationId` WHEN 12 THEN 4210 WHEN 13 THEN 18944 … ELSE 999999999 END
```

Units of the same site tie, so the cheapest one leads within it.

Two consequences are deliberate:

- Without the cookie the request **falls back to the default ordering** and says so by resetting `sort`, so
  the dropdown reflects what actually happened.
- The distance query is **not cached**: its SQL differs per visitor, and every distinct position would earn
  its own cache entry.

The browser side only asks for a position when the visitor picks a distance option and none is stored, and
does not search until it has one — otherwise the first result set would come back ordered by something else.

## Alternatives

- **Send the coordinates as query parameters.** Rejected: they would end up in shared and copied URLs, which
  the search result list is explicitly meant to be.
- **A spatial column and `ST_Distance`.** Rejected as disproportionate — the distance varies per location,
  not per storage, and there are a couple of dozen of them.
- **Sort the whole id list in PHP.** Rejected: paging and the secondary price ordering are already in SQL,
  and pulling them out would cost more than the `CASE`.
- **Ask for the position on page load.** Rejected: a permission prompt nobody asked for.

## Consequences

- Ordering by distance costs nothing measurable — the same ~80 ms as any other ordering.
- The cookie is written by JavaScript, so it carries no Yii validation hash and must be read from `$_COOKIE`;
  `Yii::$app->request->cookies` would drop it.
- The position is an hour old at most, and a visitor who declines the prompt simply gets the default order.
- `helpers/Geo.php` and `Geo.js` hold the same formula twice. The PHP side is unit tested; if one changes the
  other has to.
