From 839b0c453dc1bd63681e7119b688851f43d16c9f Mon Sep 17 00:00:00 2001
From: doxa-maps-88655c70
<88627970+doxa-maps-88655c70@users.noreply.github.com>
Date: Wed, 23 Sep 2026 23:57:31 +0000
Subject: [PATCH] A letter-portrait sheet, an honest credit, and a privacy line
FORMAT=letter. The same poster on paper a home printer takes. Width stays
1500 for the reason the 3x4 note already gives -- every width-derived
measurement (the 330 gutter, the name columns, the header budget) was
calibrated there and carries over untouched. Only height moves: 8.5x11 at
width 1500 is 1941. Layout, text and spacing are unchanged; the extra height
goes to the label columns, which is where 95 names wanted it.
PRINTS AS ONE PAGE -- and this fixes the SQUARE sheet too. The stage is sized
in CSS pixels while @page declares a physical size, so a browser mapped 1500px
to 15.6in and TILED it: measured, the 3x3 sheet printed as FOUR pages. It went
unnoticed because these posters were always screenshotted by shoot.mjs, never
printed from a browser. A scale computed at generate time (page width in px
over stage width) makes one sheet equal one page on every format. Verified via
page.pdf: letter 612x792pt x1 page, square 216x216pt x1 page (was 4).
ATTRIBUTION CORRECTED. The credit said "(c) Mapbox (c) OpenStreetMap". This
file's own header says NO Mapbox, NO token, NO external fetch at render time,
and the basemap is bundled GeoJSON -- so it credited a vendor contributing
nothing, which is 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. OSM is
named only when OSM tiles are actually fetched (BASEMAP=1).
PRIVACY LINE on the sheet. People-group coordinates arrive already generalised
from the publisher -- deliberately not precise locations. A printed sheet
travels past the person who made it, so it now carries that fact itself. Worded
as provenance, not as a claim about processing we do not perform. It gets its
own row: sharing the credit row was a measured collision (516-984px against a
stamp ending at 543), and it is a sentence to READ, not a provenance stamp.
Also: a backtick inside a CSS comment inside a JS template literal broke
generation outright -- the trap check:template-literal exists for.
---
library/scripts/posters/generate.mjs | 70 ++++++++++++++++++++++++++--
1 file changed, 65 insertions(+), 5 deletions(-)
diff --git a/library/scripts/posters/generate.mjs b/library/scripts/posters/generate.mjs
index ed57ec9a7..f655ad341 100644
--- a/library/scripts/posters/generate.mjs
+++ b/library/scripts/posters/generate.mjs
@@ -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) =>
`