Merge main into bundler-2/work
Some checks are pending
build-gate / build (push) Waiting to run

This commit is contained in:
doxa-maps-88655c70 2026-09-23 23:58:42 +00:00
commit 8653147cfc

View file

@ -255,7 +255,7 @@ const FORMATS = {
// and those already derive from MAP_BREATH/HEADER rather than hardcoded offsets.
// Serves China (154 groups) and is the sheet the shelved granular 4-column table
// (F93) was waiting on — build it once, both are served.
'3x4': { w: 1500, h: 2000, gutter: 330, page: '3in 4in' },
'3x4': { w: 1500, h: 2000, gutter: 330, page: '3in 4in', pageWidthIn: 3 },
// Square: a 3ft x 3ft sheet. The map loses height relative to 3x4, so the
// gutters widen slightly — with fewer label rows per column the names can take
// more width, and the map stays close to square rather than becoming a letterbox.
@ -273,7 +273,23 @@ const FORMATS = {
// be line wrapped — like 1 and 6 and 34, 49, 44." At 330 the name column is
// 222px and only "Vellaiyan Kuppam Padaiyaci" (265px) still wraps, which is a
// name that genuinely overruns any sane column.
'3x3': { w: 1500, h: 1500, gutter: 330, page: '3in 3in' },
'3x3': { w: 1500, h: 1500, gutter: 330, page: '3in 3in', pageWidthIn: 3 },
// LETTER PORTRAIT — the same sheet, sized for paper a normal printer can take.
// Brief: "keep the layout of the poster the same with the same text, just
// formatted for a vertical piece of paper."
//
// WIDTH STAYS 1500 for the reason the 3x4 note gives above: every width-derived
// measurement (the 330 gutter, the name columns, the header budget) was calibrated
// at 1500 and carries over UNCHANGED. Height is the only variable: 8.5x11 at
// width 1500 -> 1500 * (11/8.5) = 1941. That lands between 3x3 (1500) and 3x4
// (2000), so the map keeps roughly the 3x3 proportions with ~29% more height for
// the label columns — the sheet gets taller, nothing gets re-tuned.
//
// `page` is what the generated HTML puts in @page{size:...}. Naming the real
// paper size (rather than an inch pair) lets the browser's own Letter preset
// match it, which is what makes an ordinary printer produce a full-bleed-free,
// correctly-scaled sheet instead of "fit to page" guesswork.
letter: { w: 1500, h: 1941, gutter: 330, page: 'letter portrait', pageWidthIn: 8.5 },
};
const FMT = FORMATS[process.env.FORMAT || '3x4'] || FORMATS['3x4'];
// Stamped at generation time. Overridable so a rebuild can reproduce an existing
@ -2055,9 +2071,34 @@ async function renderPoster(zone, pins, zoneFC) {
const credit = (x, y, anchor, txt) =>
`<text x="${x.toFixed(1)}" y="${y.toFixed(1)}" text-anchor="${anchor}" class="attrib-halo">${txt}</text>`
+ `<text x="${x.toFixed(1)}" y="${y.toFixed(1)}" text-anchor="${anchor}" class="attrib">${txt}</text>`;
// ATTRIBUTION CORRECTED 2026-09-23. This said "(c) Mapbox (c) OpenStreetMap",
// which was inherited boilerplate and simply not true of this sheet: the header
// of this file states NO Mapbox, NO token, NO external fetch at render time, and
// the basemap is drawn from GeoJSON bundled in the repo. Crediting a vendor that
// contributes nothing implies a dependency that does not exist \u2014 the kind of line
// that makes a later reader add a token "to comply".
// The real source is geoBoundaries (the adm0/adm1 files carry its shapeGroup /
// shapeName fields), CC BY 4.0, which REQUIRES attribution \u2014 so the credit stays,
// it just names who actually earned it. OpenStreetMap is named only when OSM
// street tiles are actually fetched (BASEMAP=1, city sheets).
const basemapCredit = BASEMAP
? '\u00a9 geoBoundaries \u00b7 \u00a9 OpenStreetMap'
: '\u00a9 geoBoundaries (CC BY 4.0)';
// \u2605 PRIVACY LINE. The people-group coordinates arrive already generalised from
// the data publisher \u2014 they are deliberately NOT precise locations. A printed
// sheet travels far beyond the person who generated it, so it has to carry that
// fact itself rather than rely on the reader knowing it.
// Worded as provenance ("generalised at the source"), not as a claim about
// processing we perform here, because we do no rounding of our own.
const privacyNote = 'Marker positions are approximate \u00b7 people-group locations are generalised at the source for privacy';
// The privacy line gets its OWN row, 13px above the credits. Sharing the credit
// row put it at 516-984px against a stamp ending at 543 \u2014 a measured collision.
// It is also a different KIND of text: the other two are ~200px provenance
// stamps, this is a sentence a reader must actually READ, so it needs the room.
const mapCredits =
credit(FRAME_X + 8, FRAME_Y + FRAME_H - 7, 'start', `Generated ${POSTER_DATE} \u00b7 data from doxa.life`)
+ credit(FRAME_X + FRAME_W - 8, FRAME_Y + FRAME_H - 7, 'end', '\u00a9 Mapbox \u00a9 OpenStreetMap');
credit(FRAME_X + FRAME_W / 2, FRAME_Y + FRAME_H - 20, 'middle', privacyNote)
+ credit(FRAME_X + 8, FRAME_Y + FRAME_H - 7, 'start', `Generated ${POSTER_DATE} \u00b7 data from doxa.life`)
+ credit(FRAME_X + FRAME_W - 8, FRAME_Y + FRAME_H - 7, 'end', basemapCredit);
// FIT THE TITLE TO THE SHEET. Region names vary a lot in length ("West India"
// vs "Northeast India + Bangladesh + Bhutan"), and the qualifier adds a fixed
@ -2443,7 +2484,26 @@ async function renderPoster(zone, pins, zoneFC) {
/* Haloed so the credits stay legible wherever they land on the map. */
.attrib-halo{font-family:'Segoe UI',system-ui,sans-serif;font-size:9px;fill:none;
stroke:#EEF3F4;stroke-width:2.6;stroke-linejoin:round;letter-spacing:.02em}
@media print{@page{size:${FMT.page};margin:0}html,body{background:#fff}.stage{margin:0}}
/* PRINT. The stage is sized in CSS PIXELS (1500 wide) while @page declares a
PHYSICAL size, so a browser maps 1500px -> 96dpi -> 15.6in and TILES that
across the sheet: the 3x3 sheet prints as FOUR pages, not one. That went
unnoticed because these posters were built to be SCREENSHOT by shoot.mjs at
deviceScaleFactor 4, never printed from the browser.
Scaling the stage to the page's own width makes one sheet = one page on any
format, which is what a home printer needs. transform-origin top-left so the
scale collapses toward the page corner rather than the element centre. */
@media print{
@page{size:${FMT.page};margin:0}
html,body{background:#fff}
/* A FIXED ratio, computed at generate time — not a vw calc. a vw/px ratio does not
resolve against the PAGE box in print, so the calc silently evaluated to a
no-op and the sheet still tiled. The page width in CSS px is the paper width
in inches x 96; the scale is that over the stage width. */
.stage{margin:0;transform:scale(${(FMT.pageWidthIn * 96 / FMT.w).toFixed(6)});transform-origin:top left}
/* The scaled stage still reserves its ORIGINAL box, which pushes a blank second
page. Collapse the flow height to the scaled height. */
html,body{width:${(FMT.pageWidthIn * 96).toFixed(0)}px;height:${(FMT.pageWidthIn * 96 * FMT.h / FMT.w).toFixed(0)}px;overflow:hidden}
}
</style></head><body><div class="stage">${svg}</div></body></html>`;
return { html, title, octiOK, crossings, diagFrac };
}