# AGENTS.md — Precomputed place statistics

Guidance for AI agents and LLMs. **If a precomputed file exists for your (dataset, variable, place),
read it — it is one HTTP range-read instead of a minute of griddap.** Fall back to the live griddap
recipe in [`../erddap/AGENTS.md`](../erddap/AGENTS.md) only for a place/variable/date range not
covered here, or when you need data newer than the daily refresh.

## 1. Statistics for place X from the precomputed items

```sql
SELECT * FROM read_parquet('https://storage.oceanmetrics.io/gazetteer/stats/dhw_5km/CRW_SST/NMS:HIHWNMS.parquet')
ORDER BY date;
```

The path is `stats/{dataset_id}/{variable}/{place_id}.parquet` and **`place_id` keeps its colon**
(`NMS:HIHWNMS`, `PSGID:939`, `MRGID:8439`). That is legal in an S3 key and in a URL path segment, so
no escaping is needed; if some client of yours chokes on the colon, percent-encode it as `%3A` — the
object key itself never changes.

Every file carries `place_id`, `dataset_id` and `variable` columns, so the files union cleanly.
**Pass a list, not a glob**: `*` cannot be expanded over plain HTTPS (no directory listing), and
DuckDB will tell you so. Take the URLs from `collection.json`'s `rel: "item"` links.

```sql
-- warmest of these sanctuaries over the last year
SELECT place_id, avg(mean_wt) AS mean_sst
FROM read_parquet([
  'https://storage.oceanmetrics.io/gazetteer/stats/dhw_5km/CRW_SST/NMS:FKNMS.parquet',
  'https://storage.oceanmetrics.io/gazetteer/stats/dhw_5km/CRW_SST/NMS:HIHWNMS.parquet',
  'https://storage.oceanmetrics.io/gazetteer/stats/dhw_5km/CRW_SST/NMS:PMNM.parquet'])
GROUP BY place_id ORDER BY 2 DESC;
```

## 2. Discover what is precomputed

Read [`collection.json`](collection.json): `summaries` lists every `erddap-places:dataset_id`,
`erddap-places:variable` and `erddap-places:place_id`, and each `rel: "item"` link points at an Item
under `items/`. An Item gives you:

- `assets.data.href` — the Parquet file (relative to `stats/`), plus `table:columns`, `file:size`
- `properties.erddap-places:place_id` / `:dataset_id` / `:variable`
- `properties.erddap-places:griddap_url_templates` / `erddap-places:griddap_requests` — the ERDDAP
  requests behind the file (one template per lobe; `griddap_urls` too when there are ≤ 24). The
  provenance JSON's `griddap_chunks` fills `{t0}` / `{t1}` for every request
- `properties.start_datetime` / `end_datetime` — the covered window (`datetime` is `null`)
- `properties.erddap-places:status` — `complete` (the file starts at the dataset's first time step:
  the whole record up to `end_datetime`) or `partial` (it starts later: an older 365-day window, or a
  backfill still under way); `erddap-places:record_start` is that first time step
- `geometry` — the place's **lobe bounding boxes**, not its outline (the outlines run to ~40,000
  vertices; get the real polygon from [`../places/places.parquet`](../places/collection.json))
- `bbox` — the union of those lobes; for a place across ±180 it crosses with west > east
  (RFC 7946 §5): `NMS:PMNM` is about `[177.8, 19.2, -161.0, 31.8]`, not `[-180, …, 180, …]`.
  The collection extent follows the same rule

### How far the backfill has got

The full record of a daily grid takes several daily runs to fill in, a little at a time so the ERDDAP
servers are not overloaded. `collection.json` says where it stands without opening every Item:

- each `rel: "item"` link carries the Item's `erddap-places:status` (`complete` or `partial`)
- `erddap-places:item_counts` — `{complete, partial, pending}`
- `erddap-places:pending` — the targets with **no file yet** (`id`, `dataset_id`, `variable`,
  `place_id`). They have no item link, because every item link resolves to a file. Use the live
  griddap recipe for those, and for the earlier dates of a `partial` file

## 3. Continuous vs categorical

`CRW_SST` is continuous: one row per date with `mean`, `mean_wt`, `sd`, `min`, `max`, `p10`, `p90`.

`CRW_BAA` (bleaching alert area 0–4) and `CLASS` (seascape 1–33) are categorical: one row per
(date, class) with `frac_area` (area-weighted share, sums to 1 per date) and `pct_cells`. Class
labels are in the source collection's `cube:variables.<var>.erddap-places:classes`, e.g.
[`../erddap/dhw_5km/collection.json`](../erddap/dhw_5km/collection.json).

```sql
-- seascape composition of the Florida Keys, most recent date
SELECT class, round(frac_area, 3) AS frac_area
FROM read_parquet('https://storage.oceanmetrics.io/gazetteer/stats/noaa_aoml_seascapes_8day/CLASS/NMS:FKNMS.parquet')
WHERE date = (SELECT max(date) FROM read_parquet('https://storage.oceanmetrics.io/gazetteer/stats/noaa_aoml_seascapes_8day/CLASS/NMS:FKNMS.parquet'))
ORDER BY frac_area DESC;
```

## What these numbers mean

`mean` is over grid cells whose **centre** falls inside the polygon; `mean_wt` weights each cell by
the fraction of its square the polygon covers, so a coastal place with many half-covered cells is
not biased by them. Antimeridian places (`NMS:PMNM`) are fetched as two lobes and unioned before
aggregation. The window is the dataset's full record (the live extent from its first step), so `end_datetime` is the
dataset's last time step at the daily refresh, not today.

## Do not

- Do not assume a row exists for every calendar date: `noaa_aoml_seascapes_8day` steps every 8 days,
  and a cloudy/missing day simply has no row.
- Do not compare `frac_area` across places as an *area*: it is a share of that place.
- Do not read an empty file as "no data available": `NMS:MNMS` is smaller than one 5 km cell, and
  the Great Lakes sanctuaries are outside `dhw_5km`'s ocean domain. Both give 0 rows on purpose.
- Do not re-derive a place mask from the bbox in an Item's `geometry` — that is the lobe box, not
  the polygon.
