diff --git a/AGENTS.md b/AGENTS.md
index a55ca93..ba9ae3e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -910,13 +910,20 @@ session with **no other memory of this project**. So:
of an untagged pin's own coordinates** (`lib/derivedRegions.ts`, localStorage
`skatespots:derived-regions-v1`, keyed by ~110 m-rounded coords). A
`useDeriveRegions()` hook mounted on `/stats` scans the community relay for
- untagged spots/shops, fills cache misses at **1.1 s spacing** (Nominatim
- 1 req/s) and invalidates `growth-stats`; the card shows "Resolving N…" until
- done. ⚠️ This geocodes pins **when the stats page is viewed**, so Nominatim
- sees the viewer's IP + public pin coords — disclosed on `/privacy`
- ("Spot/shop geocoding"). It is a **display fallback only**; the backfill above
- is what writes real `city`/`country` tags. Either way the card's `Uncategorised`
- row now means "couldn't be tagged OR resolved", not "no tag".
+ untagged spots/shops, fills cache misses, and invalidates `growth-stats` every
+ 5 resolved (and at the end); the card shows a live "Resolving N…" counter.
+ ⚠️ **Do NOT use Nominatim for this** — it is per-VIEWER volume (every device,
+ every reload), Nominatim's policy forbids it and it **429'd us** (2026-10-01),
+ which silently broke every remaining lookup (`reverseGeocodeRegions()` returns
+ empty on `!res.ok`, so the counter froze and the card stalled). The fallback
+ now uses **BigDataCloud** `reverse-geocode-client` (no key, CORS `*` on both
+ `api.bigdatacloud.net` → 307 → `api-bdc.io`, English `countryName`/`countryCode`/
+ `city`), spaced ~300 ms. Nominatim stays for **create/edit** only (the author's
+ own pin, one call per spot). ⚠️ This geocodes pins when the stats page is
+ viewed, so the provider sees the viewer's IP + public pin coords — disclosed on
+ `/privacy` ("Spot/shop geocoding", names both providers). It is a **display
+ fallback only**; the backfill above writes real `city`/`country` tags. Either
+ way the card's `Uncategorised` row now means "couldn't be tagged OR resolved".
- **NIP-05 names — `you@skate-spots.org` (2026-09-28):** SkateSpots hands out
free handles so a new skater has a human name from minute one. The registry is
the Netlify Function **`netlify/functions/nip05.mjs`**
diff --git a/src/lib/derivedRegions.ts b/src/lib/derivedRegions.ts
index 4466630..1b14c39 100644
--- a/src/lib/derivedRegions.ts
+++ b/src/lib/derivedRegions.ts
@@ -11,15 +11,18 @@
* cached in localStorage (keyed by ~110 m-rounded coordinates), so each pin is
* reverse-geocoded at most once per device.
*
- * ⚠️ Privacy: unlike create/edit geocoding (which is the author's own pin), this
- * runs when the STATS PAGE IS VIEWED, so Nominatim sees the viewer's IP and the
- * (public) coordinates of untagged pins. Disclosed on /privacy.
+ * ⚠️ Uses **BigDataCloud** (`reverse-geocode-client`, no key, CORS, designed for
+ * browsers) — NOT Nominatim. Nominatim's usage policy forbids this kind of
+ * per-viewer volume and returned 429 (see AGENTS.md). Nominatim stays for
+ * create/edit geocoding (author-initiated, one call per spot).
+ *
+ * ⚠️ Privacy: unlike create/edit geocoding (the author's own pin), this runs when
+ * the STATS PAGE IS VIEWED, so BigDataCloud sees the viewer's IP and the (public)
+ * coordinates of untagged pins. Disclosed on /privacy.
*/
import ngeohash from 'ngeohash';
-import { reverseGeocodeRegions } from '@/lib/geocodeRegions';
-
export interface DerivedRegion {
city: string;
country: string;
@@ -27,8 +30,12 @@ export interface DerivedRegion {
}
const STORAGE_KEY = 'skatespots:derived-regions-v1';
-/** Nominatim's usage policy allows at most one request per second. */
-const GEOCODE_SPACING_MS = 1100;
+/**
+ * BigDataCloud tolerates far more than Nominatim's 1 req/s, but keep it polite:
+ * ~3 req/s. The whole list is one pass per device, then cached.
+ */
+const GEOCODE_SPACING_MS = 300;
+const REQUEST_TIMEOUT_MS = 8000;
let memoryCache: Record