# Methods

How every number on the page was produced, what it actually counts, and where it
lies. `README.md` covers what the app shows and how to run it — this file is the
provenance and caveat record. When a definition changes, change it here too.

Time base: everything is bucketed to **whole hours** and displayed in **HST
(UTC−10, no DST)**. Source APIs return UTC or HST-with-offset; the fetch scripts
normalize on the way in. The map's clock spans the union of every layer's window,
so a layer that starts later simply has no marker until its first hour.

---

## 1. Rainfall — `map_data.json`, `rainfall_{detail,summary}.csv`

**Source** [HCDP Hawaiʻi Mesonet](https://hcdp.ikewai.org),
`/mesonet/db/measurements`. Requires `HCDP_API_TOKEN`.
**Script** `hcdp_rainfall.py`
**Variable** `RF_1_Tot300s` — rainfall accumulated over each 5-minute interval, in
**millimetres**. It is a *total per interval*, not an instantaneous rate, so
summing intervals is the correct aggregation.

### Derivation

1. Fetch raw 5-minute observations for the window.
2. Drop any row with `value == null` or `flag != 0`. A nonzero flag is HCDP's
   QC rejection. In the current pull every row is flag 0, so nothing is dropped —
   the filter exists for future pulls, and `--demo` asserts it works.
3. Bucket into hourly bins by `floor((timestamp − t0) / 1h)`, summing the twelve
   5-minute values that land in each bin. `t0` is the first observation truncated
   to the hour.
4. Join station metadata (`/mesonet/db/stations`) for name, elevation, lat/lng.
   Stations with no coordinates are dropped — nothing to map.
5. `total` = sum of the whole series. `series[h]` = that hour's rainfall.

The browser computes cumulative-through-hour as a prefix sum at load time, so
scrubbing is O(1) per frame.

### Definitions

| Term in the UI | Means |
|---|---|
| **storm total** | Sum over the **entire fetch window**, not over the storm |
| **hourly rate** | mm that fell during the selected hour |
| **through this hour** | Prefix sum from window start to the selected hour |

### Caveats

- **"Storm total" is a misnomer.** The current window is **176 h, Aug 9 12 PM →
  Aug 16 7 PM HST** — about three days of ordinary windward rain before Lala's
  first band on Aug 14. The sidebar header states the window explicitly for this
  reason. A true storm-only total would need a `storm_start` field; not built.
- **77 gauges sample, they do not cover.** Windward slopes are undersampled
  relative to their rainfall. The IMERG overlay exists partly to show the ocean
  and inter-island gaps where there are no gauges at all.
- Gauge undercatch in high wind is real and uncorrected. Treat peak-hour values
  as a floor, not a ceiling.

### Island assignment (derived, not from source)

HCDP's `location` field is the string `"hawaii"` for **all 77 stations** — it
means the *state*, not the island. Every island label on the page is derived from
coordinates by `island(lat, lng)` in `hcdp_rainfall.py`, a first-match-wins list
of bounding boxes.

Order matters where boxes overlap, so the list is ordered deliberately:

- **Molokaʻi** clips Maui's northwest corner near −156.75…−156.68 → tested first.
  Without this, Keōpukaloa (−156.729) would read as Maui.
- **Kahoʻolawe** sits entirely inside the Maui box → tested first. No gauges
  there currently, but the box is correct if one appears.
- **Lānaʻi** is west of Maui's western edge, so no true overlap, but it is
  ordered before Maui anyway.

Result: Hawaiʻi 30, Maui 22, Oʻahu 12, Kauaʻi 6, Molokaʻi 5, Lānaʻi 2. No gauge
falls outside every box.

**Caveat:** boxes, not coastlines. A gauge on an offshore islet or in a channel
could land in the wrong box. Nothing in the current 77 does. The upgrade path is
point-in-polygon against Census island shapes, which adds a real dependency —
deliberately not taken.

---

## 2. Streamflow — `streams.json`

**Source** [USGS NWIS instantaneous values](https://waterservices.usgs.gov),
`stateCd=hi`. No key.
**Script** `usgs_streams.py`
**Parameters** `00060` → `flow_cfs` (discharge, cubic feet per second),
`00065` → `stage_ft` (gage height, feet).

### Derivation

NWIS returns irregular 5–15 minute values. Each is placed in its hour bin;
hours with no reading stay `null` rather than being interpolated. `flow_cfs_peak`
and `stage_ft_peak` are the max over the window. 102 sites in the current pull.

### Caveats

- **Stage is gage height, not flood stage.** It is measured from an arbitrary
  local datum, so 22.73 ft at Wailuku is not comparable to 8.87 ft at
  Manoa-Palolo. Only the *rise* is meaningful within one gauge. Adding NWS AHPS
  flood categories is the top item in `TODO.md` and would fix this.
- **Marker fill is share of that gauge's own peak**, deliberately. Coloring by
  absolute cfs would just draw a map of which basins are large.
- Provisional data. USGS revises instantaneous values for weeks afterward.
- Gaps are gaps. A `null` hour means no reading, which during a flood often means
  the gauge was damaged or submerged — not that flow was zero.

---

## 3. Storm track — `track.json`

**Source** [NHC/CPHC ATCF best track](https://ftp.nhc.noaa.gov/atcf/btk/bcp012026.dat),
storm ID `cp012026`. No key.
**Script** `nhc_track.py`

### Derivation

Parse b-deck rows where field 4 is `BEST`. The file repeats each synoptic time
once per wind-radius threshold (34/50/64 kt), so rows are deduplicated by
timestamp and the radii merged into one `radii` object of four quadrant values
each. Latitude/longitude arrive as tenths of a degree with a hemisphere letter.

- `wind_kt` is **1-minute sustained** wind, the US convention. `wind_mph` is
  `wind_kt × 1.15078`, rounded.
- `cat` is Saffir-Simpson from `wind_kt`: TD < 34 ≤ TS < 64 ≤ Cat 1 < 83 ≤
  Cat 2 < 96 ≤ Cat 3 < 113 ≤ Cat 4 < 137 ≤ Cat 5.
- ATCF writes unmeasured integer fields as blank or `0`; both are read as `null`,
  not as a real zero. This matters for `rmw`, `roci`, and `eye`.
- 29 fixes, 6-hourly. The eye marker between fixes is **linear interpolation** —
  a drawing convenience, not data.

### Peak-intensity tile

`track.peak` is the fix with the highest `wind_kt`: **75 mph / 988 mb, Cat 1,
Aug 15 8 AM HST, at 17.9 °N 155.0 °W**.

The position is rendered by `where(lat, lng)` in `index.html` as **83 mi SSE of
Ka Lae (South Point)** — nearest of seven named places, with distance from
Leaflet's `map.distance()` and a 16-point compass bearing from a flat-earth
`atan2` with a `cos(lat)` correction on longitude. Over ~100 mi the bearing error
is well under one compass point. A best-track fix is a point in open ocean;
raw coordinates tell a reader nothing, and this is how CPHC writes advisories.

### Caveats

- Best track is **post-analysis** and gets revised. It is not what was forecast at
  the time, and the app makes no forecast/cone claim.
- Wind radii are quadrant maxima, not a smooth footprint. The wind-field overlay
  drawn from them is a schematic.

---

## 4. Power outages — `outages.json`, `heco_snapshot.json`

**Source** Hawaiian Electric (HECO / MECO / HELCO).
**Script** `heco_outages.py` (live snapshot only)
**Units** the file's own `units` field: `"customers without service"`.

### What "customers" counts

**Metered accounts — service connections, not people and not addresses.**

- A single-family house is **1** customer regardless of how many people live in it.
- Apartments are usually one meter per unit, but a master-metered building can be
  **1** customer for dozens of households.
- A business, school, or pump station is **1** customer each.

So the figure systematically undercounts people affected. HECO serves roughly
500k accounts for about 1.4M residents (~2.7 people per account), which would put
the 250,900 peak somewhere near 650,000 people — an **inference, not a reported
number**, and deliberately not shown in the UI.

### Why the series is hand-entered

HECO's outage map is a Blazor app over
`https://outagemap-api-heco.azurewebsites.net/api/v1`. Auth turned out to be
free — `GET /api/v1/access-token` takes no credentials and returns a ~635-char
JWT valid 120 minutes; send it as `Authorization: Bearer <jwt>` and all three
companies answer. `heco_outages.py` does exactly that.

**But there is no history parameter.** The endpoint is a live snapshot,
overwritten roughly every 15 minutes, so it cannot reproduce the Aug 15–16 peak
after the fact. poweroutage.us blocks scripted requests (403) and gates history
behind a paid API.

So the seven timestamps in `outages.json` are **transcribed by hand from HECO
numbers given to the press** (Star-Advertiser, Hawaii News Now, Hawaiʻi Public
Radio — URLs recorded in the file's `sources`). The final point comes from the
live API. This is the weakest dataset here and the honest label is "sparse".

One useful property for future work: every active outage in a snapshot carries
`outageStartTime`, so a *single* snapshot reconstructs "customers still out, by
the hour their outage began" back to before the storm.

### Kauaʻi — KIUC (`kiuc_snapshot.json`)

**Solved, and easier than HECO.** KIUC is a member-owned co-op absent from the
HECO API, but its outage map (`https://kiuc.outagemap.coop`, an Angular app on
NISC's hosted platform) reads **three plain static JSON files off a public
bucket** — no key, no token, no referer check:

```
https://outagemap-data.cloud.coop/kiuc/Hosted_Outage_Map/summary.json
                                                        /config.json
                                                        /outage.bounds.json
```

`summary.json` has five top-level keys: `totalServed`, `lastUpdate`,
`configurationId`, `regionDataSets` (per-zip and per-county rollups), and
`outages` — each with `nbrOut`, `timeOff`, `estimateTime`, `crewAssigned`,
`lifeCycleStatus`, `x`, `y`. Timestamps are **epoch milliseconds**.
`kiuc_outages.py` fetches and tidies it.

**There are more than three files, and probing cannot tell you that.** The bucket
denies listing and returns an identical `403 AccessDenied` for keys that do not
exist, so "everything else 403s" is not evidence of absence — an earlier draft of
this file drew exactly that wrong conclusion. Reading the client bundle instead
shows names are built as `{configName}/{name}.json`, and the app requests
`summary`, `config`, `outage.bounds`, and `region.{omscounty,omszip}[.bounds]`.
The last four return 200 and carry delta-encoded Esri polygon rings in absolute
Web Mercator — real zip and county geometry, if a choropleth is ever wanted.
### What "no history" does and does not mean

Three separate statements, with different strengths. Keep them apart:

1. **Object versioning is off — proven.** `summary.json` responds with
   `x-amz-version-id: null`. A versioned object returns a real version id and
   would let you fetch prior copies via `?versionId=`. It is null, so every
   write overwrites and there are no earlier versions on the bucket. This is
   server evidence, not inference.
2. **The client never requests history — proven, but limited.** No history or
   archive call appears anywhere in the app bundle. That describes what the UI
   asks for, not what the server holds; a server can expose routes its own UI
   never calls.
3. **No history endpoint exists anywhere — NOT proven, and not provable by
   probing this host.** Every miss returns the same generic `403 AccessDenied`,
   so `/api`, `/history`, `/archive` are indistinguishable from real-but-denied
   routes. This is the same trap that produced the earlier wrong "only three
   files" claim; do not read 403 as absence.

**Open item:** whether the Internet Archive holds captures of `summary.json` is
unresolved — the Wayback CDX API was returning 503 during an Internet Archive
service outage when this was checked (2026-08-16). Odds are low for a JSON file
no page links to, but "low odds" is not "checked". Re-run:

```sh
curl "https://web.archive.org/cdx/search/cdx?url=outagemap-data.cloud.coop/kiuc/Hosted_Outage_Map/summary.json&limit=20"
```

History that may exist off-platform: poweroutage.us (co-ops included, paid API,
blocks scripted requests), EIA-417 emergency incident filings (public, coarse),
and KIUC board minutes / press releases — the same route the HECO figures came
from. Going forward, `power_watch.py`'s archive solves this prospectively; it is
not currently pointed at KIUC.

**`x`/`y` are recoverable positions**, at 1 m resolution. They are **metre
offsets from the lower-left corner of `config.json`'s `boundaryExtent`, in Web
Mercator** — not the State Plane Hawaiʻi zone 4 frame that
`mapSpatialReference` advertises. That string is a stored-but-unused leftover
describing the utility's own data, and it *cannot* describe these numbers: HI-4
has a false easting of +1,640,416 ft, so Kauaʻi would be near +1.6M feet, not
−17.8M. (`boundaryExtent == fullExtent` in this config, so either works here;
the client reads `boundaryExtent`.)

Add the extent origin and invert Web Mercator. Verified four ways:

1. The vendor's own decoder in the app bundle does exactly this
   (`jsonToSummary` adds `boundaryExtent[0]`/`[1]`; the map component hardcodes
   `spatialReference = webMercator()`), so this is the documented format, not
   an inference that happens to fit.
2. All outages land inside Kauaʻi's real bounding box, 21.87–22.22 N,
   −159.71 to −159.30 W.
3. All of them return `"Kauaʻi"` from the independent `island()` boxes in §1.
4. Points agree with `regionDataSets`' own zip rollups: the 498-customer outage
   sits in Princeville (96722, 507 out), the 119 in Kekaha/Waimea (96796, 139
   out), the 52 in Kōloa/Poʻipū (96756, 97 out). Three for three. The flipped-Y
   alternative would put the 119 on the Nā Pali coast, which has no road and no
   customers — so the orientation is settled, not assumed.

So the Kauaʻi outages **can** be mapped, with size, start time, and crew status.
That layer is not built yet.

Same snapshot limitation as HECO — `config.json` sets a 180 s refresh and there
is no history parameter — but again every outage carries its start time, so one
fetch reconstructs the curve. Snapshot taken **Aug 16, 7:53 PM HST**:

| | |
|---|---|
| Customers served (Kauaʻi) | 36,344 |
| Active outages | 100 |
| Customers out | 968 |
| Largest single outage | 498 customers, began Aug 16 11:01 AM |

Customers still out, by the hour their outage began — 949 of the 968 began on
Aug 16, with a sharp spike in the 11 AM hour (537):

```
Aug 16  06 AM  119     11 AM  537     04 PM    2
        07 AM   14     12 PM   56     05 PM    6
        08 AM   23     01 PM   20     06 PM    5
        09 AM   32     02 PM   51     07 PM    6
        10 AM   69     03 PM    9
```

**Do not read that curve as an outage level.** It is "customers *still out at
7:53 PM* whose outage began in hour H" — survivorship-filtered, so it is a lower
bound at every hour and it systematically over-weights long-lived outages.

An earlier draft of this file claimed the 11 AM bin corroborated the HECO
statewide peak hour. **That claim is withdrawn.** 498 of the 537 customers in
that bin — 93% — are a *single* outage still unrestored 8.5 hours later. The
bin is one long-lived event, not evidence of an 11 AM island-wide hit. The
median outage in the snapshot is 1 customer and 76 of 100 are single-customer,
so the total is dominated by a handful of large survivors. Falsifying this would
need repeated snapshots, which is what the archive in `power_watch.py` is for.

19 customers trace to outages that began as far back as Jul 30 and are unrelated
to the storm.

### Zip and county polygons (`kiuc_regions.geojson`)

**Script** `kiuc_regions.py` → 13 zip polygons + 1 county polygon, joined to the
snapshot's `regionDataSets` counts and rendered as the map's optional
"Kauaʻi outages by zip" choropleth.

Note which files these are. `region.omszip.json` and `region.omscounty.json`
carry the named features. The sibling `region.*.bounds.json` files are *not* the
same thing — each is a single unnamed feature whose first ring is a world-sized
rectangle, an inverse mask the app draws to grey out everything off-territory.
No names, no per-region geometry; ignored.

**Wire format — delta-encoded Esri rings.** Each ring is one flat array
`[x0, y0, dx1, dy1, dx2, dy2, …]`. The first pair is an absolute Web Mercator
coordinate in metres; every pair after is a delta **from the running vertex**,
not from the origin. Integers, so 1 m resolution. Ring winding carries meaning
as in any Esri polygon: clockwise rings are exteriors, counter-clockwise rings
are holes in the preceding exterior — `kiuc_regions.py` reproduces that with a
shoelace test and emits GeoJSON MultiPolygons with holes attached correctly.
One zip (96716) genuinely has two disjoint parts.

Validated: all 13 zip polygons fall entirely inside the independent `island()`
box for Kauaʻi from §1. Only the county ring pokes outside, at 4 vertices of
~24,600 — correct, since county boundaries extend offshore.

The layer is **off by default and not on the clock**. It is one snapshot, and a
polygon that sits still while the slider moves would read as "no change here"
rather than "no data here". The legend says so inline.

Worth reading off it: Princeville (96722) at **12.7% out** and Waimea (96796) at
**12.9%** dwarf everywhere else — Līhuʻe (96766) is 0.26%. The damage was
concentrated on the north and west shores, not island-wide.

### Three traps in this feed

- **Planned outages are mixed in.** `lifeCycleStatus` is `"Planned Outage"` on a
  couple of records. `kiuc_outages.py` excludes them from `customers_out` and
  reports the count in `planned_excluded` — currently 2 customers, immaterial
  during a storm but not on a quiet day.
- **Most ETAs are already expired.** `estimateTime` is never null in the live
  feed, and ~87 of 101 are in the past as of `lastUpdate`. Read naively they look
  like forecasts. Each record carries an `eta_expired` flag for this.
- **`lastUpdate` is the authoritative snapshot time** — use it, not wall-clock
  fetch time. It is now carried through as `snapshot_time`.

### Two numerators, two denominators

The feed does not agree with itself, and neither figure is silently preferred:

| | Out | Served |
|---|---:|---:|
| Point records (planned excluded) | 969 | — |
| `regionDataSets` county rollup | 931 | 35,654 |
| `regionDataSets` zip rollup | 931 | 35,651 |
| `totalServed` | — | 36,344 |

The two rollups agree with each other and disagree with the points by 38; the
served counts sit ~690 below `totalServed`, and `config.json` sets
`useConsumerCountForTotalServed`, which suggests meters-vs-consumers. KIUC
publicly reports 33,000+ billed meters and ~38,695 member-owners, so 36,344 is
plausible between them. Quote it as **"KIUC's own served count"**, not a verified
customer total.

### Why KIUC is not summed into `statewide`

`statewide` in `outages.json` is **HECO service territory only** (HELCO + MECO +
HECO), matching what HECO reports to the press. KIUC is a separate co-op with
its own reporting, so it is carried as its own series and excluded from the sum.
The file records this in a `statewide_scope` field, and the chart tooltip says
"HECO territory out" rather than "statewide".

This rule was applied to fix a real inconsistency: the Aug 16 14:30 row summed
the three HECO islands (191,800, correctly excluding Kauaʻi's 1,824) while the
19:31 row had folded Kauaʻi in (161,933 instead of 161,024). All eight rows now
satisfy `statewide == hawaii + maui + oahu`, asserted after each edit. It **does
not change the peak-outage tile** — still 250,900 at Aug 16 11 AM, a row where
Kauaʻi was never reported.

The chart tooltip and the peak tile now say "HECO territory" / "HECO only" so
the scope is visible rather than implied.

### Caveats

- **Kauaʻi is not in `outages.json`.** The KIUC snapshot is a separate file and
  is not yet merged into the statewide series, so the chart's `statewide` line
  remains a HECO-only total and understates the real one by roughly 1,000 at the
  time of the snapshot.
- `null` means "not broken out in that advisory", not zero.
- Seven points across two days is not a time series. The chart draws it sparse on
  purpose rather than interpolating a smooth curve that was never observed.

---

## 5. Coastal — `coast.json`

**Script** `coast.py`

### Surge — NOAA CO-OPS tide gauges

**Observed water level minus predicted tide**, which isolates the part of the
water the storm put there. Both series are pulled at datum **MLLW**, metric,
GMT, and differenced timestamp-by-timestamp; the hourly value is the **max**
difference within that hour. 7 stations: Nāwiliwili, Honolulu, Mokuʻoloe,
Kahului, Kawaihae, Hilo, Midway.

This is a *residual*, not a formal storm-surge product: it also contains any
error in the tide prediction, plus wave setup and seiche. It is the right
first-order signal, not an official surge value.

### Waves — NDBC buoys

Significant wave height (`WVHT`, column 9 of the `realtime2` fixed-width text),
in metres, hourly max. `MM` means missing. 14 buoys probed, 9 reachable;
unreachable ones are dropped rather than drawn empty.

`realtime2` carries no position, so buoy lat/lng comes from line 2 of
`latest_obs/<id>.txt`, parsed from degrees + decimal minutes. That is the buoy's
*current* position — a drifting buoy would be plotted slightly off.

Peak observed: 5.5 m (18.0 ft) at buoy 51206, southeast of Hawaiʻi Island.

---

## 5b. Gridded rainfall — `rain_grid.json`, `rain_grid/*.png`

**Source** HCDP interpolated daily rainfall raster, same token as §1.
**Script** `rainfall_raster.py`

```
GET https://api.hcdp.ikewai.org/raster
    ?datatype=rainfall&production=new&period=day&extent=statewide&date=YYYY-MM-DD
```

77 gauges *sample* the islands; this **covers** them, at ~250 m. It is the answer
to the undersampling caveat in §1 — with the caveat that it is itself an
interpolation of those same gauges, not independent measurement.

- **`period=day` only.** `month`, `hour`, and `6hour` all 404, so this cannot be
  put on the hourly clock. The map's "That day's total" mode swaps at HST
  midnight and holds; the key names the day being shown.
- **~1 day lag.** The current day 404s until processed. Aug 16 — the day of the
  Oʻahu outage peak — was not yet available, which is why the overlay currently
  shows rain on Hawaiʻi Island and almost nothing on Oʻahu.
- **Per-island extents exist** (`bi`, `mn`, `ka`, `oa`); statewide covers all.
- **Nodata is `-3.4e38`**, the float32 fill. Unmasked, every statistic returns
  `-inf`. Roughly 8% of the statewide grid is land; the rest is that fill.

**Reprojection is mandatory.** Source is EPSG:4326 at 0.00225°/px; Leaflet's
screen space is Web Mercator and `L.imageOverlay` stretches an image linearly
between two corners, so a raw 4326 grid lands ~10 km out in the middle of the
image — coastlines visibly off the basemap. GDAL/rasterio does the warp
(bilinear, nodata-aware). An earlier draft hand-rolled a nearest-neighbour
version of this; GDAL was installed the whole time.

Output is one RGBA PNG per day plus a summed total, 16–40 KB each — smaller than
the JSON that describes them, because most of the frame is transparent.

Daily peaks: **Aug 13** 16.3 mm · **Aug 14** 255.4 mm (10.1 in) · **Aug 15**
621.4 mm (24.5 in). Three-day total peak **674 mm (26.5 in)** — higher than any
gauge reports for the same window, which is what an interpolated surface between
gauges is supposed to show.

## 6. Satellite overlays — `satellite.json`

**Source** [NASA GIBS WMTS](https://gibs.earthdata.nasa.gov). No key.
**Script** `satellite.py`

Three layers: GOES-West ABI GeoColor (hourly), VIIRS SNPP corrected-reflectance
true color (daily, ~1:30 PM overpass), and IMERG precipitation rate (hourly,
mm/h).

**The archive has holes.** Requesting a timestamp GIBS does not have returns 404
and paints an empty overlay, so the script **probes one cheap tile per candidate
frame** (z5/y13/x4, covering the main islands) with a HEAD request and writes
only the hits. The app then snaps the clock to the nearest existing frame. GOES
currently has 137 of 176 hours; IMERG is denser; VIIRS is 8 daily frames.

IMERG is a **satellite estimate**, not a measurement — it is included precisely
because it covers the ocean between the islands where no gauge exists, but it
should never be read as gauge-equivalent.

---

## Derived-in-browser quantities

Everything below is computed in `index.html` and exists in no source file.

| Quantity | How |
|---|---|
| Cumulative rainfall | Prefix sum of `series` at load |
| Peak 1-hour rainfall | `max(series)` per station — **94.9 mm (3.74 in) at Kaiholena, hour ending Aug 15 8 PM HST** |
| Island label | `island(lat, lng)` bounding boxes (§1) |
| Peak-fix location | `where(lat, lng)` nearest-place + bearing (§3) |
| Stream marker fill | `flow_cfs[h] / flow_cfs_peak` for that gauge |
| Inches | `mm / 25.4`, display only — ramps, radii, and all internal math stay in mm |
| Eye position between fixes | Linear interpolation |

---

## Known limitations, ranked

1. **Outages are seven hand-transcribed points**, and the Kauaʻi/KIUC snapshot is
   not yet merged into the statewide series.
2. **Stage has no flood-stage context**, so hydrographs are unreadable as
   severity. Fixing this (NWS AHPS) is the top `TODO.md` item.
3. **"Storm total" spans the whole 176 h fetch window**, including pre-storm rain.
4. **Island labels are bounding boxes**, not coastlines.
5. **77 gauges undersample** the windward slopes that got the most rain.
6. USGS values are **provisional** and will be revised.
7. Surge is an **observed-minus-predicted residual**, not an official product.

---

## Changelog

Newest first. Record what changed about *method or definition*, not cosmetics.

- **2026-08-16** — KIUC feed re-audited after adversarial review. Corrected three
  errors in the entry below: `x`/`y` **are** convertible to lat/lng (Web Mercator
  offsets, not State Plane), the bucket holds **more than three files** (403 there
  is indistinguishable from missing, so probing proved nothing), and the claim
  that KIUC's 11 AM bin corroborated the HECO peak hour was **withdrawn** as
  survivorship bias. Also picked up `lastUpdate`, `lifeCycleStatus`, expired-ETA
  flagging, and the feed's two disagreeing numerators.
- **2026-08-16** — `statewide` defined as HECO territory only and made consistent
  across all rows; Kauaʻi carried as its own series rather than summed in.
- **2026-08-16** — KIUC (Kauaʻi) outages located and fetched: NISC's hosted map
  serves `summary.json` publicly with no auth, and its start times reconstruct a
  lower-bound curve.
- **2026-08-16** — Peak-intensity tile gained a location (`where()`); the fix
  had lat/lng all along and simply was not rendered.
- **2026-08-16** — Island names derived from lat/lng bounding boxes. Previously
  every gauge displayed HCDP's `location` field, which is `"hawaii"` for all 77
  and made Maui and Oʻahu gauges read as Hawaiʻi Island.
- **2026-08-16** — Sidebar and header now state the rainfall window explicitly,
  because "storm total" was being read as storm-only.
- **2026-08-16** — Inches toggle added as a display-time conversion; all internal
  values stay in mm.
