fuzzyworld

when-am-i / files / plan.md

plan.md

Blame: the commit that last changed each line · file · history

26a02b71 2026-06-19 Pavel Safronov1# Plan — When am I? (formerly "What Year Is It?")
d96c425a 2026-06-17 Pavel Safronov2
3Status as of 2026-06-16. Owner: Pavel.
4
5## Goal of this milestone
6
7Ship an **end-to-end MVP**: a user picks a location on the map and a **full date**
8(year + month + day, BCE/CE), and the app resolves the matching region and era and
9displays the **local calendar year** using **real calendar math** (not just the era's
10metadata). Build the engine with **Vitest tests** and add **ESLint**.
11
12Boston is the proof-of-architecture region; the loop must generalize to any region file.
13
14---
15
16## Where we are today (baseline)
17
18Working:
19- [App.tsx](src/App.tsx): header + intro + map; echoes clicked lat/lng.
20- [src/components/MapPicker.tsx](src/components/MapPicker.tsx): the live map (`center=[20,0]`, `zoom=2`).
26a02b71 2026-06-19 Pavel Safronov21- Data: region files [massachusett.json](src/data/calendars/massachusett.json)
22 (Massachusett reckoning) and [colonial-america.json](src/data/calendars/colonial-america.json)
23 (empire-wide Julian/Gregorian); schema docs in
d96c425a 2026-06-17 Pavel Safronov24 [SCHEMA.json](src/data/calendars/SCHEMA.json) / [SCHEMA.md](src/data/calendars/SCHEMA.md).
a6030ee4 2026-10-27 Pavel Safronov25- Deploy: [.fw-ci.yml](.fw-ci.yml) builds and publishes `dist/` to GitLab Pages.
d96c425a 2026-06-17 Pavel Safronov26
27Missing / debt:
28- **No date picker.**
29- **No resolution engine**: nothing loads the calendar JSON, runs point-in-polygon,
30 matches an era, or converts/displays a local year.
30de6faf 2026-06-20 Pavel Safronov31- ~~**No build-time data validation** (README claims it exists — it does not).~~
32 **Resolved 2026-06-20:** `npm run validate:data`
33 ([scripts/validate-data.mts](scripts/validate-data.mts)) now validates every
34 region file and runs in `prebuild` + CI, so the README claim is true. See the
35 data-pipeline note below.
d96c425a 2026-06-17 Pavel Safronov36- **Dead code**: [src/MapPicker.tsx](src/MapPicker.tsx) is an unused, broken duplicate
37 (no `center`/`zoom`) of the real component.
38- **Empty stubs**: [src/data/index.ts](src/data/index.ts), [src/hooks/index.ts](src/hooks/index.ts),
39 [src/types/index.ts](src/types/index.ts), [src/utils/index.ts](src/utils/index.ts).
40- **No tests, no lint.**
41
42---
43
44## Architecture for the MVP
45
26a02b71 2026-06-19 Pavel Safronov46Core framing (from Q4): *"When am I?"* is addressed to **a person**, so the answer
d96c425a 2026-06-17 Pavel Safronov47is **plural**. The engine does not pick one era — it **collects every valid answer** a local
48could give. Ask a Boston settler and an Algonquian person in 1700 and you get two answers;
49both are valid and we show both.
50
51Data flow: `click (lat,lng)` + `date` → **find all region(s)** containing the point →
52within each, **find all eras** whose date range covers the date (eras may overlap) →
53**convert** the date into each era's calendar (or fall back to descriptive-only) →
54**render the full list of answers**.
55
56### 1. Types — [src/types/index.ts](src/types/index.ts)
57Mirror the JSON schema and add result types:
58- `Geometry` (GeoJSON Polygon/MultiPolygon; coords are `[lng, lat]`).
59- `Region { name; geo: Geometry }`.
60- `Era { name; start; end; calendar; usedBy?; description?; sources? }` — `start`/`end` are
61 `YYYY-MM-DD | null`; new optional **`usedBy`** labels *whose* answer it is
62 (e.g. "British colonists", "Algonquian peoples"), which the plural model needs.
63- `CalendarRegion { region: Region; eras: Era[] }`.
64- `HistoricalDate` — see "Date representation" below.
65- `Answer { region: Region; era: Era; conversion: CalendarConversion }` — one possible
66 reply a local could give.
67- `Resolution { answers: Answer[]; missReason?: 'no-region' | 'no-era' }` — the engine
68 returns the **list** of answers (possibly empty, with a reason).
69- `CalendarConversion { system: string; localYearLabel: string; localDateLabel?: string; convertible: boolean }`
70 — `convertible: false` ⇒ descriptive-only (see §5).
71
72### 2. Data loading — [src/data/index.ts](src/data/index.ts)
73- Load all region files with `import.meta.glob('./calendars/*.json', { eager: true })`,
74 excluding `SCHEMA.json`.
75- Export a typed `regions: CalendarRegion[]`.
76- Runtime shape-guard so a malformed file fails loudly (complements build-time validation).
77
78### 3. Geometry — `src/utils/geo.ts`
79- `pointInPolygon(lat, lng, geometry): boolean` — ray-casting, supports `Polygon` and
80 `MultiPolygon`, respects GeoJSON `[lng, lat]` ordering and ring holes.
81- `findRegions(lat, lng, regions): CalendarRegion[]` — return **all** matches (regions
82 can overlap; UI must handle 0, 1, or many).
83
84### 4. Era matching — `src/utils/eras.ts`
85- `findEras(date, eras): Era[]` — return **all** eras whose half-open interval `[start, end)`
86 covers the date (`null` start = open past, `null` end = open present). **Eras may overlap**
87 (Q4): parallel calendars coexisting at the same place and time is expected, not an error.
88- Compare via Julian Day Number (see Date representation) so BCE/CE and proleptic-Gregorian
89 boundaries compare correctly.
90
91### 5. Calendar conversion — `src/utils/calendars.ts`
92- `convert(date, calendarName): CalendarConversion`.
93- Registry keyed by the era's `calendar` string. Two independent gates (Q3):
94 - **Acceptance** of a calendar into the app is a *data* decision (your rule: a wiki page
95 with enough sourced info). Not enforced here.
96 - **Convertibility** is whether a deterministic epoch+rule exists to compute a numeric
97 local year. Observational calendars with no continuous count (e.g. "Algonquian lunar
98 calendar") are **descriptive-only**: return `convertible: false`, still show the era,
99 its name/description/sources and the Gregorian year for reference, with a clear
100 "no continuous numeric year to convert to" note — **never silently mislabel**.
101- Convertible set (Q1): Gregorian + hand-rolled Julian, plus `@js-temporal/polyfill`
102 systems (Islamic, Hebrew, Persian, Japanese, Chinese, …).
103- **MVP scope for descriptive-only** (decided): render `description` + `sources` links
104 (e.g. a Wikipedia explanation) + Gregorian reference year. **No extra per-calendar data
8961f70b 2026-06-20 Pavel Safronov105 needed** — the schema already has `sources`. The rich answer's **lunar phase** has since
106 shipped (computed, no data needed); the remaining pieces (moon *name* +
107 "winters since event") are still a **stretch goal**, see below.
d96c425a 2026-06-17 Pavel Safronov108
109### 6. UI
110- `src/components/DatePicker.tsx`: year + month + day inputs + BCE/CE toggle; emits a
111 `HistoricalDate`. Validate ranges (days-in-month, no year 0 in historical numbering).
112- `src/components/ResultPanel.tsx`: renders **the list of answers** — for each, *whose*
113 answer (`usedBy`), the calendar system, the local year/date (or descriptive-only note),
114 description, and sources. Framing: "Ask a British colonist → …; ask an Algonquian
115 person → …". Plus miss states (no region / no era covering the date).
116- [App.tsx](src/App.tsx): hold `{ lat, lng }` + `HistoricalDate`, call the engine, render the panel.
117
118### 7. Cleanup
119- Delete [src/MapPicker.tsx](src/MapPicker.tsx) (dead duplicate).
120- Replace empty `index.ts` stubs with real exports or delete the unused ones.
121
122---
123
124## Data pipeline (lightweight, to make the README honest)
125
30de6faf 2026-06-20 Pavel Safronov126**Shipped 2026-06-20** as [scripts/validate-data.mts](scripts/validate-data.mts)
127(`npm run validate:data`), wired into `prebuild` and CI.
128**Decision:** rather than promote [SCHEMA.json](src/data/calendars/SCHEMA.json)
129to a real JSON Schema / zod schema, the validator itself is the authoritative,
130machine-checkable schema. A hand-rolled validator covers semantic checks a plain
131JSON Schema cannot express — dates that actually parse, geometry rings that are
132closed with in-range `[lng, lat]`, and `[start, end)` ordering — and avoids a
133second spec that could drift from the validator. SCHEMA.json / SCHEMA.md remain
134the human-readable field docs and now point at the validator. Overlapping eras
135are intentionally **not** flagged (Q4).
136
137- ~~Promote [SCHEMA.json](src/data/calendars/SCHEMA.json) to a real **JSON Schema** (or a
138 zod schema) and~~ add `npm run validate:data` that checks every region file:
d96c425a 2026-06-17 Pavel Safronov139 required fields, valid geometry, parseable proleptic-Gregorian dates. **Overlapping eras
140 are allowed** (Q4) — do not flag them.
141- Add the optional `usedBy` field to the schema (back-compatible).
26a02b71 2026-06-19 Pavel Safronov142- Model **coexistence**: around colonization the colonial calendars and the Massachusett
143 lunar reckoning apply at the same place and time, each tagged with `usedBy`. (Now split
144 across two overlapping regions — [massachusett.json](src/data/calendars/massachusett.json)
145 and [colonial-america.json](src/data/calendars/colonial-america.json) — rather than one
146 Boston file.)
d96c425a 2026-06-17 Pavel Safronov147- Wire validation into `prebuild` and into CI so bad data can't deploy.
148
149---
150
151## Verify tooling (adopt from `~/code/share`)
152
153Reuse Pavel's established pattern (reference: `~/code/share/web/scripts/*.mts`):
154- A `.mts` Playwright script run via `tsx` that **auto-starts the Vite dev server**,
155 forces deterministic app state via **URL query params** (here: a `?demo=` that pre-sets
156 picked location + date so the answers panel renders without manual clicking), stabilizes
157 any volatile content, screenshots to `screenshots/<category>/`, and **checks them in**.
158- ImageMagick `magick` composites for documentation imagery.
159- Most valuable once the ResultPanel exists → schedule in **Phase 4** (UI). The `?demo=`
160 state hook should be designed into App during Phase 4, not bolted on later.
161
162### Per-calendar PR screenshots
163A `screenshot:calendar -- <file.json>` script that renders **one calendar's** answers so a
164**calendar PR can include illustrative screenshots showing any major issues**:
165- Compute a point **inside the region** (polygon centroid / point-on-surface) and force the
166 app there via `?demo=` with a **representative date per era** (e.g. midpoint of each era's
167 range). Screenshot the answers panel for each.
168- Optionally a single composite per calendar (map + answers) via `magick`.
169- This makes review concrete: a wrong region polygon, an off-by-a-century era boundary, a
170 miscategorized "convertible vs descriptive-only", or a broken conversion all show up
171 **visually** in the PR. Reviewer (Pavel) checks the screenshot against Wikipedia/facts.
172- Document this in the contributor section so each calendar PR attaches its screenshots.
173 Depends on the `?demo=` hook + ResultPanel → **Phase 4+**.
174
175## Testing & lint
176
177- **Vitest** + config; unit tests for: `pointInPolygon` (inside/outside/edge/MultiPolygon),
178 `findEras` (boundaries, open intervals, **overlapping eras**, BCE), `convert` (known
179 date↔year pairs per system, descriptive-only fallback), and a data-validation test that
180 loads every region file.
181- **ESLint** (flat config, TS + react-hooks) + `npm run lint`.
182- Add `test`, `lint`, `validate:data` scripts to [package.json](package.json).
a6030ee4 2026-10-27 Pavel Safronov183- Update [.fw-ci.yml](.fw-ci.yml): run `lint` + `test` before `build`.
d96c425a 2026-06-17 Pavel Safronov184
185---
186
187## Documentation gate (required before "done")
188
189- README: document the date picker, the resolution engine, the plural-answers model, the
190 list of **convertible vs descriptive-only** calendars, the new scripts (`test`, `lint`,
191 `validate:data`), and the real validation step (replacing the current aspirational claim).
192- Keep [SCHEMA.md](src/data/calendars/SCHEMA.md) in sync with the promoted JSON Schema.
193- Note any data-format changes back into the contributor section.
194
195---
196
197## Phased execution
198
1991. **Foundations** — types, data loader, delete dead `MapPicker`, Vitest + ESLint scaffolding, CI.
2002. **Engine** — geo + era matching + conversion registry, with tests. Gregorian/Julian first.
2013. **Library-backed calendars** — wire the chosen conversion lib (Q1), add tests.
2024. **UI** — DatePicker + ResultPanel + App wiring; handle all miss states.
c7d226e1 2026-06-20 Pavel Safronov2035. **Data pipeline** — ✅ `validate:data` + `prebuild`/CI gate (see note above; the
204 validator is the schema, no separate JSON Schema file).
2056. **Docs** — README + SCHEMA sync. ✅ Generalization proven 2026-06-20 with three
206 region files beyond Boston:
207 [iran.json](src/data/calendars/iran.json) (Solar Hijri + lunar Hijri, exercising the
208 Temporal-backed Persian and Islamic converters),
209 [russian-america.json](src/data/calendars/russian-america.json) (a second Julian→Gregorian
210 cutover, at the 1867 Alaska transfer), and
211 [oregon-country.json](src/data/calendars/oregon-country.json) (Hudson's Bay Company,
212 covering the Seattle area). Note: the Temporal **`chinese`** calendar was the
213 first pick for the convertible-library proof but is broken in
214 `@js-temporal/polyfill@0.5.1` (throws on every date — "Unexpected leap month
215 suffix"; no fixed version available), so **Persian/Iran** was used instead.
d96c425a 2026-06-17 Pavel Safronov216
483f1c57 2026-06-21 Pavel Safronov217 **The five most-populous calendars added 2026-06-20** (covering the most
218 humans × time, per Pavel's request): [china.json](src/data/calendars/china.json)
219 (Chinese sexagenary cycle — descriptive-only, hand-rolled stem-branch/zodiac
220 naming, since the Temporal lunisolar `chinese` is broken),
221 [india.json](src/data/calendars/india.json) (Saka, existing Temporal converter),
222 [japan.json](src/data/calendars/japan.json) (a pre-1873 lunisolar
223 descriptive era + the modern Japanese **nengō** era, Temporal `japanese` using
224 `era`/`eraYear`, scoped to Meiji+),
225 [thailand.json](src/data/calendars/thailand.json) (Buddhist Era, existing
226 converter), and [ethiopia.json](src/data/calendars/ethiopia.json) (Ethiopian
227 calendar, **hand-rolled via JDN** because the Temporal `ethiopic` *and*
228 `coptic` calendars also throw in this polyfill — same class of bug as
229 `chinese`). New converters live in
230 [src/utils/calendars.ts](src/utils/calendars.ts) with tests.
231
d96c425a 2026-06-17 Pavel Safronov232---
233
234## Stretch goals (post-MVP)
235
c7d226e1 2026-06-20 Pavel Safronov236### AI-assisted dataset authoring (requested 2026-06-20)
483f1c57 2026-06-21 Pavel Safronov237**✅ Shipped 2026-06-20** as the `new-calendar` skill
238([.claude/skills/new-calendar/SKILL.md](.claude/skills/new-calendar/SKILL.md)):
239it walks an author through researching a place/people's calendar(s) with sourced
240citations, drafting the GeoJSON polygon and eras, picking the right converter (or
241descriptive-only), and running `validate:data` / `gen:badges` /
242`screenshot:calendar` so the result is PR-ready — with the project's "never
243invent, always source" rule front and centre. A review/QA skill remains a
244possible follow-up. Original sketch of what the skill does:
c7d226e1 2026-06-20 Pavel Safronov245- Take a place + people/polity as input (e.g. "the Mughal Empire", "Coast Salish around
246 Puget Sound") and **research** the calendar system(s), era boundaries, and *whose*
247 reckoning it is (`usedBy`), gathering **citable sources** (the project's acceptance gate
248 — never invent events or moon names; see "Data accuracy" below).
249- Draft the **region polygon** (GeoJSON `[lng, lat]`, closed rings) and the eras with
250 proleptic-Gregorian `start`/`end`, choosing the right `calendar` string so it hits an
251 existing converter (or flagging it as descriptive-only).
252- Decide **convertible vs descriptive-only** correctly, and if a new convertible system is
253 needed, note the converter work in [src/utils/calendars.ts](src/utils/calendars.ts)
254 (with the `@js-temporal/polyfill` caveat — e.g. `chinese` is currently broken).
255- Run `npm run validate:data`, `npm run gen:badges`, and
256 `npm run screenshot:calendar -- <file>` so the output is PR-ready with review screenshots.
257- Possibly a second, smaller skill to **review/QA** an existing dataset against its sources.
258This is a tooling/authoring aid, not app runtime; keep it in `.claude/`, not shipped to users.
259
73912a47 2026-06-20 Pavel Safronov260### In-browser map creation/editing (requested 2026-06-20)
261Let contributors **draw and edit region polygons directly in the app**, instead of
262hand-editing GeoJSON or round-tripping through geojson.io. Lowers the contribution bar
263a lot (the current flow asks people to author raw `[lng, lat]` rings by hand). Initially
264felt heavy-handed (a full path-editing UI), but it now pairs naturally with the other
265authoring/build-time tooling being considered:
266- A Leaflet drawing layer (e.g. `leaflet-draw` / `react-leaflet` editing) to add/move/delete
267 polygon vertices on the existing map, emitting schema-shaped GeoJSON.
268- An **export** path: produce a ready-to-commit region JSON (or a prefilled GitLab
269 merge-request / issue) so a drawn map becomes a contribution without local tooling.
270- Naturally combines with **land-clipping** (below) so a roughly-drawn coastal polygon is
271 auto-trimmed to land, and with **AI-assisted authoring** (above) so the AI can draft a
272 polygon the user then nudges by hand.
273- Open question: purely a contributor/authoring tool vs. a playful end-user feature
274 ("draw your own region"). Keep authoring output reproducible either way.
275
276### Scalable calendar colors (requested 2026-06-20)
483f1c57 2026-06-21 Pavel Safronov277**✅ Shipped 2026-06-20** in [src/utils/calendarColor.ts](src/utils/calendarColor.ts).
278The new `calendarColorScale(names)` assigns colors to the *set* of calendars on
279screen at once: curated calendars keep their `PALETTE` color, the rest are walked
280in a stable (sorted) order and handed **golden-angle-spaced** hues so they fan out
281around the wheel instead of colliding on a raw name hash. [App.tsx](src/App.tsx)
282builds one scale from every calendar in the dataset, so a calendar keeps the same
283color across the map, the timeline, and as you scrub dates (legend ↔ map ↔ timeline
284agree). `calendarColor(name)` is retained for a lone name / fallback (now also
285golden-angle-based). Possible later polish: contrast/accessibility tuning so no
286generated color vanishes on the map tiles. Original framing of the problem:
73912a47 2026-06-20 Pavel Safronov287- Space hues apart (e.g. golden-angle increments) instead of a raw name hash, and/or
288 assign colors **per render** so the calendars actually on screen are maximally distinct.
289- Keep determinism where it matters (legend ↔ map agreement within a view) and keep the
290 curated palette for the handful of well-known calendars (or drop it if a good generator
291 makes it redundant — that would touch [calendarColor.test.ts](src/utils/calendarColor.test.ts)).
292- Consider contrast/accessibility (avoid colors that vanish on the map tiles).
293
d96c425a 2026-06-17 Pavel Safronov294### Rich descriptive-only answers
295For non-convertible calendars, replace the plain "no year count" note with the kind of
296answer a real local would give. Example target for **Boston, 1700-04-01**, asked of a
297**Massachusett person**:
298
299> "We don't count years that way — but it is the **[early-spring] moon**, **~3 days after a
300> new moon**, and about **N winters since [a remembered event]**."
301
302This is authentic *in structure*. Its pieces split sharply by cost:
303
304- **Computable for free (no per-region data):**
8961f70b 2026-06-20 Pavel Safronov305 - **Lunar phase / days since new moon** — *shipped*. `lunarPhaseLabel` in
306 `src/utils/calendars.ts` computes the night-of-the-moon + phase from the Julian Day
307 Number; the "Algonquian lunar calendar" answer now leads with it (still
308 `convertible: false`, no year) instead of the bare "no year count" note.
483f1c57 2026-06-21 Pavel Safronov309 - **Eclipse-anchored "winters since"** — superseded by the per-region
310 `anchorEvents` approach below (eclipses turned out to need either a heavy
311 ephemeris or the same curated data, so they're just one *kind* of anchor).
312- **"Winters since" via per-region anchor events — ✅ Shipped 2026-06-20.** Pavel's
313 call (the most reliable option): each anchor must be a **documented, sourced**
314 event supplied in the data; the engine only counts the winters, never invents
315 one. Implemented as an optional `Era.anchorEvents` (`{ name, date, sources }[]`),
316 with `wintersSince()` in [src/utils/calendars.ts](src/utils/calendars.ts)
317 picking the most recent anchor at/before the date and reporting "about N winters
318 since &lt;event&gt;" (counted as completed calendar years so anniversaries aren't
319 off-by-one). Wired through [resolve.ts](src/utils/resolve.ts) →
320 `CalendarConversion.reckoning` → a sourced line in
321 [ResultPanel.tsx](src/components/ResultPanel.tsx); validated in
322 `validate-data.mts` and the data loader; documented in SCHEMA.md/README.
323 [massachusett.json](src/data/calendars/massachusett.json) carries the first
324 anchor (the 1616–1619 epidemic → "about 84 winters since" in 1700).
325- **Still needs curated, sourced per-region data (the expensive part — hence stretch):**
d96c425a 2026-06-17 Pavel Safronov326 - **Moon names** — vary by nation, inconsistently attested; must be cited, not invented.
483f1c57 2026-06-21 Pavel Safronov327 - More **historical-event anchors** for other descriptive calendars — must be real,
328 dateable, locally remembered events, **not** invented ones (the "asteroid crash" in
329 the brainstorm was illustrative only — no such event occurred).
330- **Schema:** the `anchorEvents` part shipped (above); optional `reckoning` text and a
331 `moons` list remain optional + back-compatible future additions.
d96c425a 2026-06-17 Pavel Safronov332
333### Data accuracy
334- Refine Boston's generic `"Algonquian lunar calendar"` to the specific **Massachusett**
335 people (via `usedBy`), with sources. "Algonquian" is a language family, not one calendar.
336
337---
338
339## Resolved decisions
340
341- **Q1 — Conversion library.** ✅ `@js-temporal/polyfill` for library-backed systems
342 (Islamic, Hebrew, Persian, Japanese, Chinese, …) **plus a hand-rolled Julian↔Gregorian**
343 (Temporal has no Julian calendar).
344- **Q2 — Historical date representation.** ✅ JSON `start`/`end` are **proleptic Gregorian**
345 (rule written into SCHEMA.md). Internally normalize to a **Julian Day Number** for all
346 comparisons so BCE/CE and pre-1582 dates sort correctly. **UTC is not involved** — it is a
347 time-of-day/timezone standard, not a calendar; we resolve dates, not clock time, and avoid
348 JS `Date` in the engine (it mishandles BCE and drags in timezones).
349- **Q3 — Descriptive-only calendars.** ✅ Two gates: *acceptance* (data decision: a sourced
350 wiki page is enough) vs *convertibility* (a deterministic epoch+rule exists). Calendars
351 that pass acceptance but not convertibility (e.g. Algonquian lunar) are shown with their
352 name/description/sources + the Gregorian year for reference and a clear "no continuous
353 numeric year to convert to" note. Never hidden, never mislabeled. (See "Algonquian
354 answers" below for the nuance the UI should convey.)
355- **Q4 — Plural answers / overlap.** ✅ The engine returns **all** valid answers.
356 Geographic region overlap → show all matched regions. Eras within a region **may overlap
26a02b71 2026-06-19 Pavel Safronov357 in time** → show all concurrent calendars, because "When am I?" is asked of a person
d96c425a 2026-06-17 Pavel Safronov358 and different people legitimately answer differently.
b92a4280 2026-06-20 Pavel Safronov359- **Q5 — Modern global default.** ✅ (implemented 2026-06-20, [src/utils/resolve.ts](src/utils/resolve.ts))
360 The Gregorian calendar is the world's near-universal *civil* calendar today, so for dates
361 from **1927** onward (chosen cutoff — just after the last major national adoption, Turkey
362 1926) the engine appends a synthetic **"most of the world" Gregorian answer** anywhere on
363 Earth, unless a region already supplies a Gregorian answer for that date (dedup, no double
364 Gregorian). So most modern clicks resolve even with no specific region, and a place with a
365 local calendar shows both (e.g. modern Iran → Persian **and** Gregorian). It is engine-only:
366 **not** a region polygon, so it never shades the map (a world polygon would flood it); a
367 matching band is mirrored onto the year timeline. A single global cutoff is a deliberate
368 simplification (adoption was staggered: 1582/1752/1918/1926/1949/2016…), hence "most of
369 the world", not a universal claim. The cutoff is one constant (`MODERN_DEFAULT_START_YEAR`).
370 Open follow-up: the default currently applies over oceans too — could be land-bounded once
371 the land-clipping work (stretch goals) lands.