# Bharat Desa — Interactive Inscription Map (300–1800 CE)

उत्तरं यत्समुद्रस्य हिमाद्रेश्चैव दक्षिणम् । वर्षं तद् भारतं नाम भारती यत्र संततिः ॥
*The land south of the Himalaya and north of the ocean is Bhārata.*

An SVG map of South Asia's inscriptions: filter by year-capsule
(incl. Date-not-available) and tag-capsules, zoom and pan,
click seals for full records with transcription and translation.
Starts at 320 CE — empty Bharat canvas, no realms yet. Light/dark
icon-only toggles. No dependencies, no build step to *view* — just
open the file.

## Quick start

```bash
open map-app/index.html   # double-click works too — no server needed.
# Record tabs lazy-load their source JSON via static/records/*.js chunks
# (<script> injection works on file:// and http alike).
```

## Files

| File | Role |
|---|---|
| `index.html` | Layout + **inlined accurate base map** (amCharts country paths, ocean, Bharat boundary). Self-contained on purpose — works from `file://`. |
| `styles.css` | White/black themes (`html[data-theme]`), faint modern base, dashed Bharat boundary, marker/label sizes. |
| `app.js` | Vanilla renderer. Projects `data.js` lon/lat → SVG, capsule filters, zoom-compensated labels, tooltips, tabbed popup. |
| `data.js` | **All living history data**: `EMPIRES`, `PLACES`, `EVENTS`, `RANGES`, `PERIODS`, `PROJ` (projection fit). Edit this to grow the map. |
| `app.js` | Vanilla renderer (Lucide icons inline for the theme toggle). |
| `static/` | Source of truth for *super-static* geometry (see below). |
| `svg maps/` | Untouched amCharts source SVGs (India, neighbours, world). Never edited. |

## Baseline mode (current): inscriptions + timelines only

`app.js` has one flag:

```js
const BASELINE_INSCRIPTIONS_ONLY = true;
```

While true, the map shows **only inscription seals + the time controls**:
realm polygons, legend, battles and non-inscription places are hidden,
but **no kingdom code or data was deleted** — set the flag `false` and
everything returns. What you see:

- **~5,050 inscription records at ~920 seals** (full Smriti corpus via
  `static/build_all.py`) + 6 hand-curated in `data.js`. Uniform Lucide scroll-text seals (2× size), ink-black /
  ink-white with halo, no badge — except an iMessage-style red count
  bubble on multi-record seals. Badge, tooltip and popup all count
  bucket+tag-filtered records.
  Labels are small and position themselves (right → left → above →
  below) to avoid overlap.
  Labels are small and position themselves (right → left → above →
  below) to avoid overlap.
- Hover a seal → place + count + summary tooltip. Click a
  multi-record seal → **table overview** (Year | Inscription+summary |
  Source); click a row → **tabbed record**, with **← Prev / i of n / Next →**
  stepping through sibling records without returning to the table.
  Tabs: Summary (narrative, Keyword/People/Places pills, Notes, full
  Catalogue + Attributes + Publication tables, beside a half-width
  locator map in true map colours) · Translation · Transcription ·
  Image (estampage/drive photos, PDF links) · Record (source file
  rendered as formatted tables/pills/text — never raw JSON).
  Popup covers the map, translucent (X / ESC closes).
- **Time = multi-select capsules** (`BUCKETS` in `data.js`, date-only
  labels): 300–600, 600–750, 750–900, 900–1100, 1100–1300, 1300–1750,
  plus **Date not available** — toggle any combination (last one
  sticks); a seal shows when *any* of its records is in a selected
  bucket.
- **Tags = second capsule row** (`TAG_FILTERS`): Maitraka·Valabhi,
  Rashtrakuta, Chalukya, Chola, Pandya, Chahamana, Gurjara-Pratihara,
  Pallava, Land grants — diacritic-insensitive whole-word matching
  over tags+title+place, empty selection passes everything; combined
  with Year by AND. **Map layers = third row** (`LAYER_DEFS`):
  Rivers, Ranges toggles. Counts, popups and tables all follow every filter.
- **Zoom**: +/−/reset buttons (Lucide), double-click to reset,
  mouse-wheel zoom at cursor, drag-pan (drags swallow the follow-up
  click so seals don't misfire). Strokes and type **counter-scale**
  with zoom (CSS `--z` for static layers, seal/label re-render), so
  rivers stay hairline and labels stay readable at any depth.
  Zoom-out clamps before the context runs out.

## Bulk import: `static/build_all.py` (full Smriti corpus)

```bash
python3 static/build_all.py   # → static/inscriptions.js (IMPORTED only, no RECORDS blob)
```

- Fetches nothing itself — reads `../inscriptions/*.json` (all 5,072
  Smriti detail records, fetched via
  `https://smriti-api-vjllehz3fq-el.a.run.app/api/inscriptions/{id}`).
- Location resolution per record: hand `GAZETTEER` (from
  `import_inscriptions.py`) wins → valid `Approx Latlong` inside India →
  otherwise Nominatim geocode of village/taluk/district/state with a
  persistent `static/geocode_cache.json` (1 req/s, resumable; re-run to
  continue). Report in `static/geo_report.json` (places / events /
  unplaced).
- BCE dates (`c. 2nd century B.C.E.` → negative years) get their own
  `bbce` year capsule in `data.js`.
- Output keeps the `IMPORTED = { places, events }` shape, but full
  source JSONs are **not** embedded (5k records would be ~60 MB).
  The Record tab fetches `../inscriptions/<key>.json` on demand — so
  serve over HTTP (`python3 -m http.server`), not `file://`.
- 5k seals stay fast via viewport culling + grid clustering in
  `app.js` (`clusterItems`): dense cells collapse to one seal with a
  count badge; click zooms in until seals split apart.

## Legacy import: `static/import_inscriptions.py`

```bash
python3 static/import_inscriptions.py   # → static/inscriptions.js
```

- Parses CE dates, century phrases (`late 8th century` → 785),
  Saka (+78) / Gupta-Valabhi (+319) / Chalukya-Vikrama (+1076) eras
  from any date field or the title. 10 regnal-year records
  (`13th year, Tai.` …) keep `year: null` with the original label —
  they show on the map but need epigraphist review for exact years.
- Findspots resolve through the `GAZETTEER` (lon/lat + approx flag;
  an assert fails loudly on out-of-range coords — it once caught every
  seal landing in the ocean from a lat/lon swap). Reuses existing
  place ids (Valabhi, Bhubaneswar) so seals aggregate; skips
  0005/0288/0320 (already hand-curated in `data.js`).
- Output `const IMPORTED = { places, events }` (timeline fields,
  tags, entities, images, PDF links, infobox, attributes) plus
  `const RECORDS = { key: full JSON }` (~1.3 MB total — keeps the
  Record tab working from `file://` with no fetch). Merges into
  `PLACES`/`EVENTS` at startup with id-dedup (hand data wins).
  Verify with `node landcheck.js` (all seals must print `OK`).

## How to extend

Open `data.js` → push to `EMPIRES`, `PLACES`, `EVENTS`.
No other file needs to change.

**Places need real lon/lat:**
```js
{ id: "paithan", name: "Paithan", ancient: "Pratishthana",
  lon: 75.39, lat: 19.48, type: "inscription",   // capital|city|port|fort|temple|inscription
  empire: "rashtrakuta",
  desc: "Findspot of Govinda III plates (794 CE)." },
```

**Empires need `poly: [[lon,lat]…]`, `start/end` years:**
```js
{ id: "pala", name: "Pala Empire (Rahma)", short: "Pala", color: "#4da96b",
  start: 750, end: 1162, capital: "Monghyr (Mudgagiri)",
  rulers: "Dharmapala · Devapala",
  poly: [[84,26.5],[88,26.8],[91.5,25.5],…],
  desc: "…", sources: "…" },
```
Polygons are schematic zenith extents; the year slider shows an empire
iff `start ≤ year ≤ end`. Keep vertex order clockwise, ~6–16 points.

**Events link a `placeId` + `year` + optional `link`:**
```js
{ id: "e-paithan", title: "Paithan Plates of Govinda III",
  year: 794, yearLabel: "Saka 716 · solar eclipse · 794 CE",
  kind: "inscription",            // inscription (✎ square) | battle (▲)
  placeId: "paithan",
  summary: "…", source: "Ep. Ind. Vol.3 Art.17",
  link: "../inscriptions/inscription_0288.json" },   // optional; "" = no link
```

Rivers and ranges:
```js
// RANGES (data.js) — crest lines, same pattern:
{ name: "Himavant (Himalaya)", labelAt: [85.6,28.4], pts: […] }
// RIVERS are static: 30 Natural Earth 10m rivers live in
// static/rivers.svg (built by static/build.py). To add/rename one,
// edit the WANT table there and re-run python3 static/build.py.
```

Label priority on the map is `capital → inscription → temple → fort → port → city`;
dots always draw, colliding labels auto-hide (details stay one click away).

## Static data (boundaries live here)

Super-static geometry is **not** hand-edited into `index.html`. It lives in
`static/` and is generated into the page:

| File | Content |
|---|---|
| `static/base-countries.svg` | Every world country intersecting the max zoom-out viewport (lon 15–145, lat −30…65 — currently ~115, from Arabia to Japan to N. Australia), one uniform land tone. Auto-selected by bbox in `build.py`; specks dropped. |
| `static/rivers.svg` | 30 rivers from Natural Earth 10m (`ne_rivers.geojson`, filtered to the `WANT` table, projected + simplified). 10 majors labeled: Sindhu, Ganga, Yamuna, Lauhitya, Narmada, Tapti, Godavari, Krishna, Kaveri, Mahanadi, Sutlej. |
| `static/ne_rivers.geojson` | Untouched Natural Earth 10m source (7 MB, public domain). |
| `static/bharat-boundary.svg` | Outer boundary + faint-orange fill (`bharat-fill` layer below empires, dashed `bharat-line` above, exposed as `#bharatMain` for the popup mini locator maps). Boundary and modern borders are always on (no toggles). Union of IN+PK+BD+NP+BT+LK+AF+MM plus real Xizang/Tibet from `svg maps/chinaHigh.svg` (CN-XZ, fitted from its own cropped Mercator space — validated against the Himalayan crest). Rendered dashed, international-border style. |
| `static/build.py` | Regenerates both SVGs from `svg maps/worldIndiaHigh.svg` and inlines them (+ ocean rect, tighter viewBox `630 344 126 106`) into `index.html`. Idempotent — safe to re-run. |

```bash
python3 static/build.py     # needs: python3 -m pip install shapely
```

## Projection (why overlays align)

All overlays are stored as **real lon/lat** and projected in `app.js`
with a Mercator fit calibrated against the amCharts base
(least-squares over 14 lon + 10 lat control points, residuals < 0.3 px):

```
x = 2.6653·lon + 480.03
y = −151.948·ln(tan(π/4 + rad(lat)/2)) + 460.00
```

Validated: 23/23 places fall inside their correct modern boundary,
14/14 empire centroids on land. Self-checks: Sir Creek 23.9°N,
Kibithu 28.0°N, J&K claim tip 37.1°N.

## Notes & limits

- Empire frontiers are interpretive (praśasti claims are hyperbolic).
  Treat polygons as zenith extents, not surveyed borders (empire layer
  currently parked behind the baseline flag).
- Rivers/ranges are schematic crest/thalweg lines, not surveyed courses.
- Google Earth/Maps imagery can't be embedded (ToS + API key + raster
  tiles wouldn't align anyway). For real terrain relief, the path is a
  DEM hillshade baked to SVG offline — a possible future layer.
- `svg maps/` sources are amCharts geodata (modern borders, orientation only).

## Sources

K. M. Munshi, *The Glory That Was Gurjara Desa* · Elliot & Dowson, Vol. 1 ·
Arab geographers (Sulayman, Ibn Khurdadhbih, al-Masudi) · Xuanzang *Si-Yu-Ki* ·
Kalhana *Rajatarangini* · *Epigraphia Indica* Vols 3–4 · inscription records in `../inscriptions/` ·
base geography: amCharts · rivers: Natural Earth 10m (public domain) ·
icons: Lucide (ISC, inlined moon/sun — no CDN, works from `file://`).
