@escape-game-over/atlas 0.1.73 → 0.1.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escape-game-over/atlas",
3
- "version": "0.1.73",
3
+ "version": "0.1.74",
4
4
  "type": "module",
5
5
  "description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
6
6
  "private": false,
@@ -0,0 +1,669 @@
1
+ import type { CountryCode } from "./countries.ts";
2
+
3
+ /** A part of `PostalAddress` an address layout can print. */
4
+ export type AddressField = "street" | "locality" | "region" | "postalCode";
5
+
6
+ /**
7
+ * One piece of a line: a field, or text the country always prints.
8
+ *
9
+ * A field brings its own punctuation, so a missing field takes it with it and
10
+ * nothing is left to clean up:
11
+ *
12
+ * - `separator` sits between this field and whatever the line already holds,
13
+ * and is dropped when the field opens the line: `, ` before a US state.
14
+ * - `prefix` belongs to the value itself and goes wherever it goes: the `CH-`
15
+ * of a Swiss postal code.
16
+ */
17
+ export type AddressPart =
18
+ | {
19
+ readonly field: AddressField;
20
+ readonly separator?: string;
21
+ readonly prefix?: string;
22
+ }
23
+ | { readonly text: string };
24
+
25
+ /** A country's address, line by line. */
26
+ export type AddressLayout = readonly (readonly AddressPart[])[];
27
+
28
+ /**
29
+ * How a country lays out an address.
30
+ *
31
+ * Converted once from Google's address metadata, the dataset behind
32
+ * libaddressinput (Apache License 2.0), fetched 2026-10-02 one country at a
33
+ * time: <https://chromium-i18n.appspot.com/ssl-address/data/DE>, the `fmt`
34
+ * field. The bare `/data/` path answers `{}`. Google writes each layout as a
35
+ * format string, `%A%n%C, %S %Z`; this is the same layout as data, so nothing
36
+ * parses a string at run time.
37
+ *
38
+ * The conversion:
39
+ * - `%A` street
40
+ * - `%C` locality
41
+ * - `%S` region
42
+ * - `%Z` postal code
43
+ * became fields; text before a line's first field became that field's
44
+ * `prefix`, and text between two fields the second one's `separator`; a line of
45
+ * text alone became a `text` part. The recipient lines (`%N`, `%O`) and the
46
+ * fields `PostalAddress` does not carry — district, sorting code, India's
47
+ * extra lines — were dropped, with the punctuation that belonged to them.
48
+ *
49
+ * Countries that share a layout share a branch, in order of their first code.
50
+ * To update one, take its format from that address again and convert it the
51
+ * same way. A country listed twice is caught by Biome's `noDuplicateCase`.
52
+ */
53
+ export function addressLayout(country: CountryCode): AddressLayout {
54
+ switch (country) {
55
+ case "AD":
56
+ case "AT":
57
+ case "BA":
58
+ case "BE":
59
+ case "BG":
60
+ case "BL":
61
+ case "CY":
62
+ case "CZ":
63
+ case "DE":
64
+ case "DK":
65
+ case "DO":
66
+ case "DZ":
67
+ case "EH":
68
+ case "ET":
69
+ case "FR":
70
+ case "GE":
71
+ case "GF":
72
+ case "GL":
73
+ case "GP":
74
+ case "GR":
75
+ case "GW":
76
+ case "IS":
77
+ case "KG":
78
+ case "KW":
79
+ case "LA":
80
+ case "LR":
81
+ case "MA":
82
+ case "ME":
83
+ case "MF":
84
+ case "MG":
85
+ case "MK":
86
+ case "MQ":
87
+ case "NC":
88
+ case "NE":
89
+ case "NL":
90
+ case "NO":
91
+ case "PL":
92
+ case "PM":
93
+ case "PT":
94
+ case "PY":
95
+ case "RE":
96
+ case "RS":
97
+ case "SJ":
98
+ case "SK":
99
+ case "SM":
100
+ case "SN":
101
+ case "TJ":
102
+ case "TM":
103
+ case "TN":
104
+ case "TZ":
105
+ case "VA":
106
+ case "WF":
107
+ case "XK":
108
+ case "YT":
109
+ case "ZM":
110
+ return [
111
+ [{ field: "street" }],
112
+ [
113
+ { field: "postalCode" },
114
+ { field: "locality", separator: " " },
115
+ ],
116
+ ];
117
+ case "AE":
118
+ case "NR":
119
+ return [[{ field: "street" }], [{ field: "region" }]];
120
+ case "AF":
121
+ case "AI":
122
+ case "FK":
123
+ case "GB":
124
+ case "GS":
125
+ case "IM":
126
+ case "IO":
127
+ case "KE":
128
+ case "LK":
129
+ case "NA":
130
+ case "PN":
131
+ case "SD":
132
+ case "SH":
133
+ case "SZ":
134
+ case "TC":
135
+ case "TT":
136
+ case "VG":
137
+ case "ZA":
138
+ return [
139
+ [{ field: "street" }],
140
+ [{ field: "locality" }],
141
+ [{ field: "postalCode" }],
142
+ ];
143
+ case "AL":
144
+ case "EC":
145
+ case "MU":
146
+ case "OM":
147
+ return [
148
+ [{ field: "street" }],
149
+ [{ field: "postalCode" }],
150
+ [{ field: "locality" }],
151
+ ];
152
+ case "AM":
153
+ return [
154
+ [{ field: "street" }],
155
+ [{ field: "postalCode" }],
156
+ [{ field: "locality" }],
157
+ [{ field: "region" }],
158
+ ];
159
+ case "AR":
160
+ case "CL":
161
+ case "CV":
162
+ case "MY":
163
+ case "UZ":
164
+ return [
165
+ [{ field: "street" }],
166
+ [
167
+ { field: "postalCode" },
168
+ { field: "locality", separator: " " },
169
+ ],
170
+ [{ field: "region" }],
171
+ ];
172
+ case "AS":
173
+ case "AU":
174
+ case "CA":
175
+ case "CC":
176
+ case "CX":
177
+ case "FM":
178
+ case "HM":
179
+ case "MH":
180
+ case "MP":
181
+ case "NF":
182
+ case "PW":
183
+ case "UM":
184
+ case "VI":
185
+ return [
186
+ [{ field: "street" }],
187
+ [
188
+ { field: "locality" },
189
+ { field: "region", separator: " " },
190
+ { field: "postalCode", separator: " " },
191
+ ],
192
+ ];
193
+ case "AX":
194
+ return [
195
+ [{ field: "street" }],
196
+ [
197
+ { field: "postalCode", prefix: "AX-" },
198
+ { field: "locality", separator: " " },
199
+ ],
200
+ [{ text: "ÅLAND" }],
201
+ ];
202
+ case "AZ":
203
+ return [
204
+ [{ field: "street" }],
205
+ [
206
+ { field: "postalCode", prefix: "AZ " },
207
+ { field: "locality", separator: " " },
208
+ ],
209
+ ];
210
+ case "BB":
211
+ case "SO":
212
+ case "US":
213
+ return [
214
+ [{ field: "street" }],
215
+ [
216
+ { field: "locality" },
217
+ { field: "region", separator: ", " },
218
+ { field: "postalCode", separator: " " },
219
+ ],
220
+ ];
221
+ case "BD":
222
+ return [
223
+ [{ field: "street" }],
224
+ [
225
+ { field: "locality" },
226
+ { field: "postalCode", separator: " - " },
227
+ ],
228
+ ];
229
+ case "BH":
230
+ case "BM":
231
+ case "BN":
232
+ case "BT":
233
+ case "GU":
234
+ case "IL":
235
+ case "JO":
236
+ case "KH":
237
+ case "LB":
238
+ case "LS":
239
+ case "MT":
240
+ case "MV":
241
+ case "NP":
242
+ case "NZ":
243
+ case "SA":
244
+ case "VC":
245
+ return [
246
+ [{ field: "street" }],
247
+ [
248
+ { field: "locality" },
249
+ { field: "postalCode", separator: " " },
250
+ ],
251
+ ];
252
+ case "BR":
253
+ return [
254
+ [{ field: "street" }],
255
+ [{ field: "locality" }, { field: "region", separator: "-" }],
256
+ [{ field: "postalCode" }],
257
+ ];
258
+ case "BS":
259
+ case "KN":
260
+ return [
261
+ [{ field: "street" }],
262
+ [{ field: "locality" }, { field: "region", separator: ", " }],
263
+ ];
264
+ case "BY":
265
+ return [
266
+ [{ field: "street" }],
267
+ [
268
+ { field: "postalCode" },
269
+ { field: "locality", separator: ", " },
270
+ ],
271
+ [{ field: "region" }],
272
+ ];
273
+ case "CH":
274
+ return [
275
+ [{ field: "street" }],
276
+ [
277
+ { field: "postalCode", prefix: "CH-" },
278
+ { field: "locality", separator: " " },
279
+ ],
280
+ ];
281
+ case "CI":
282
+ return [
283
+ [{ field: "street" }, { field: "locality", separator: " " }],
284
+ ];
285
+ // No space between region and town, as both write them.
286
+ case "CN":
287
+ case "TW":
288
+ return [
289
+ [{ field: "postalCode" }],
290
+ [{ field: "region" }, { field: "locality" }],
291
+ [{ field: "street" }],
292
+ ];
293
+ case "CO":
294
+ return [
295
+ [{ field: "street" }],
296
+ [
297
+ { field: "locality" },
298
+ { field: "region", separator: ", " },
299
+ { field: "postalCode", separator: ", " },
300
+ ],
301
+ ];
302
+ case "CR":
303
+ return [
304
+ [{ field: "street" }],
305
+ [{ field: "region" }, { field: "locality", separator: ", " }],
306
+ [{ field: "postalCode" }],
307
+ ];
308
+ case "CU":
309
+ return [
310
+ [{ field: "street" }],
311
+ [{ field: "locality" }, { field: "region", separator: " " }],
312
+ [{ field: "postalCode" }],
313
+ ];
314
+ case "EE":
315
+ case "ES":
316
+ case "IT":
317
+ case "PF":
318
+ case "UY":
319
+ return [
320
+ [{ field: "street" }],
321
+ [
322
+ { field: "postalCode" },
323
+ { field: "locality", separator: " " },
324
+ { field: "region", separator: " " },
325
+ ],
326
+ ];
327
+ case "EG":
328
+ case "IE":
329
+ case "RU":
330
+ case "UA":
331
+ return [
332
+ [{ field: "street" }],
333
+ [{ field: "locality" }],
334
+ [{ field: "region" }],
335
+ [{ field: "postalCode" }],
336
+ ];
337
+ case "FI":
338
+ return [
339
+ [{ field: "street" }],
340
+ [
341
+ { field: "postalCode", prefix: "FI-" },
342
+ { field: "locality", separator: " " },
343
+ ],
344
+ ];
345
+ case "FO":
346
+ return [
347
+ [{ field: "street" }],
348
+ [
349
+ { field: "postalCode", prefix: "FO" },
350
+ { field: "locality", separator: " " },
351
+ ],
352
+ ];
353
+ case "GG":
354
+ return [
355
+ [{ field: "street" }],
356
+ [{ field: "locality" }],
357
+ [{ text: "GUERNSEY" }],
358
+ [{ field: "postalCode" }],
359
+ ];
360
+ case "GI":
361
+ return [
362
+ [{ field: "street" }],
363
+ [{ text: "GIBRALTAR" }],
364
+ [{ field: "postalCode" }],
365
+ ];
366
+ case "GN":
367
+ return [
368
+ [
369
+ { field: "postalCode" },
370
+ { field: "street", separator: " " },
371
+ { field: "locality", separator: " " },
372
+ ],
373
+ ];
374
+ case "GT":
375
+ return [
376
+ [{ field: "street" }],
377
+ [
378
+ { field: "postalCode" },
379
+ { field: "locality", separator: "- " },
380
+ ],
381
+ ];
382
+ case "HK":
383
+ return [
384
+ [{ field: "region" }],
385
+ [{ field: "locality" }],
386
+ [{ field: "street" }],
387
+ ];
388
+ case "HN":
389
+ case "IQ":
390
+ return [
391
+ [{ field: "street" }],
392
+ [{ field: "locality" }, { field: "region", separator: ", " }],
393
+ [{ field: "postalCode" }],
394
+ ];
395
+ case "HR":
396
+ return [
397
+ [{ field: "street" }],
398
+ [
399
+ { field: "postalCode", prefix: "HR-" },
400
+ { field: "locality", separator: " " },
401
+ ],
402
+ ];
403
+ case "HT":
404
+ return [
405
+ [{ field: "street" }],
406
+ [
407
+ { field: "postalCode", prefix: "HT" },
408
+ { field: "locality", separator: " " },
409
+ ],
410
+ ];
411
+ case "HU":
412
+ return [
413
+ [{ field: "locality" }],
414
+ [{ field: "street" }],
415
+ [{ field: "postalCode" }],
416
+ ];
417
+ case "ID":
418
+ case "MN":
419
+ case "TH":
420
+ case "VN":
421
+ return [
422
+ [{ field: "street" }],
423
+ [{ field: "locality" }],
424
+ [{ field: "region" }, { field: "postalCode", separator: " " }],
425
+ ];
426
+ case "IN":
427
+ case "NG":
428
+ case "PE":
429
+ return [
430
+ [{ field: "street" }],
431
+ [
432
+ { field: "locality" },
433
+ { field: "postalCode", separator: " " },
434
+ ],
435
+ [{ field: "region" }],
436
+ ];
437
+ case "IR":
438
+ return [
439
+ [{ field: "region" }],
440
+ [{ field: "locality" }],
441
+ [{ field: "street" }],
442
+ [{ field: "postalCode" }],
443
+ ];
444
+ case "JE":
445
+ return [
446
+ [{ field: "street" }],
447
+ [{ field: "locality" }],
448
+ [{ text: "JERSEY" }],
449
+ [{ field: "postalCode" }],
450
+ ];
451
+ case "JM":
452
+ case "PA":
453
+ case "SC":
454
+ case "SR":
455
+ case "TV":
456
+ return [
457
+ [{ field: "street" }],
458
+ [{ field: "locality" }],
459
+ [{ field: "region" }],
460
+ ];
461
+ case "JP":
462
+ return [
463
+ [{ field: "postalCode", prefix: "〒" }],
464
+ [{ field: "region" }],
465
+ [{ field: "street" }],
466
+ ];
467
+ case "KI":
468
+ return [
469
+ [{ field: "street" }],
470
+ [{ field: "region" }],
471
+ [{ field: "locality" }],
472
+ ];
473
+ case "KP":
474
+ case "KZ":
475
+ return [
476
+ [{ field: "postalCode" }],
477
+ [{ field: "region" }],
478
+ [{ field: "locality" }],
479
+ [{ field: "street" }],
480
+ ];
481
+ case "KR":
482
+ return [
483
+ [{ field: "region" }, { field: "locality", separator: " " }],
484
+ [{ field: "street" }],
485
+ [{ field: "postalCode" }],
486
+ ];
487
+ case "KY":
488
+ return [
489
+ [{ field: "street" }],
490
+ [{ field: "region" }, { field: "postalCode", separator: " " }],
491
+ ];
492
+ case "LI":
493
+ return [
494
+ [{ field: "street" }],
495
+ [
496
+ { field: "postalCode", prefix: "FL-" },
497
+ { field: "locality", separator: " " },
498
+ ],
499
+ ];
500
+ case "LT":
501
+ return [
502
+ [{ field: "street" }],
503
+ [
504
+ { field: "postalCode", prefix: "LT-" },
505
+ { field: "locality", separator: " " },
506
+ { field: "region", separator: " " },
507
+ ],
508
+ ];
509
+ case "LU":
510
+ return [
511
+ [{ field: "street" }],
512
+ [
513
+ { field: "postalCode", prefix: "L-" },
514
+ { field: "locality", separator: " " },
515
+ ],
516
+ ];
517
+ case "LV":
518
+ return [
519
+ [{ field: "street" }],
520
+ [{ field: "region" }],
521
+ [
522
+ { field: "locality" },
523
+ { field: "postalCode", separator: ", " },
524
+ ],
525
+ ];
526
+ case "MC":
527
+ return [
528
+ [{ field: "street" }],
529
+ [
530
+ { field: "postalCode", prefix: "MC-" },
531
+ { field: "locality", separator: " " },
532
+ ],
533
+ ];
534
+ case "MD":
535
+ return [
536
+ [{ field: "street" }],
537
+ [
538
+ { field: "postalCode", prefix: "MD-" },
539
+ { field: "locality", separator: " " },
540
+ ],
541
+ ];
542
+ case "MM":
543
+ return [
544
+ [{ field: "street" }],
545
+ [
546
+ { field: "locality" },
547
+ { field: "postalCode", separator: ", " },
548
+ ],
549
+ ];
550
+ case "MO":
551
+ return [[{ field: "street" }]];
552
+ case "MX":
553
+ return [
554
+ [{ field: "street" }],
555
+ [
556
+ { field: "postalCode" },
557
+ { field: "locality", separator: " " },
558
+ { field: "region", separator: ", " },
559
+ ],
560
+ ];
561
+ case "MZ":
562
+ return [
563
+ [{ field: "street" }],
564
+ [
565
+ { field: "postalCode" },
566
+ { field: "locality", separator: " " },
567
+ { field: "region" },
568
+ ],
569
+ ];
570
+ case "NI":
571
+ return [
572
+ [{ field: "street" }],
573
+ [{ field: "postalCode" }],
574
+ [{ field: "locality" }, { field: "region", separator: ", " }],
575
+ ];
576
+ case "PG":
577
+ return [
578
+ [{ field: "street" }],
579
+ [
580
+ { field: "locality" },
581
+ { field: "postalCode", separator: " " },
582
+ { field: "region", separator: " " },
583
+ ],
584
+ ];
585
+ case "PH":
586
+ return [
587
+ [{ field: "street" }],
588
+ [{ field: "locality" }],
589
+ [{ field: "postalCode" }, { field: "region", separator: " " }],
590
+ ];
591
+ case "PK":
592
+ return [
593
+ [{ field: "street" }],
594
+ [
595
+ { field: "locality" },
596
+ { field: "postalCode", separator: "-" },
597
+ ],
598
+ ];
599
+ case "PR":
600
+ return [
601
+ [{ field: "street" }],
602
+ [
603
+ { field: "locality" },
604
+ { field: "postalCode", separator: " PR " },
605
+ ],
606
+ ];
607
+ case "RO":
608
+ return [
609
+ [{ field: "street" }],
610
+ [
611
+ { field: "postalCode" },
612
+ { field: "region", separator: " " },
613
+ { field: "locality", separator: " " },
614
+ ],
615
+ ];
616
+ case "SE":
617
+ return [
618
+ [{ field: "street" }],
619
+ [
620
+ { field: "postalCode", prefix: "SE-" },
621
+ { field: "locality", separator: " " },
622
+ ],
623
+ ];
624
+ case "SG":
625
+ return [
626
+ [{ field: "street" }],
627
+ [{ field: "postalCode", prefix: "SINGAPORE " }],
628
+ ];
629
+ case "SI":
630
+ return [
631
+ [{ field: "street" }],
632
+ [
633
+ { field: "postalCode", prefix: "SI-" },
634
+ { field: "locality", separator: " " },
635
+ ],
636
+ ];
637
+ case "SV":
638
+ return [
639
+ [{ field: "street" }],
640
+ [
641
+ { field: "postalCode" },
642
+ { field: "locality", separator: "-" },
643
+ ],
644
+ [{ field: "region" }],
645
+ ];
646
+ case "TR":
647
+ return [
648
+ [{ field: "street" }],
649
+ [
650
+ { field: "postalCode" },
651
+ { field: "locality", separator: " " },
652
+ { field: "region", separator: "/" },
653
+ ],
654
+ ];
655
+ case "VE":
656
+ return [
657
+ [{ field: "street" }],
658
+ [
659
+ { field: "locality" },
660
+ { field: "postalCode", separator: " " },
661
+ { field: "region", separator: ", " },
662
+ ],
663
+ ];
664
+ // Google's default: BF and MW, and every country it gives no layout of
665
+ // its own.
666
+ default:
667
+ return [[{ field: "street" }], [{ field: "locality" }]];
668
+ }
669
+ }
package/src/contact.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { addressLayout } from "./address-formats.ts";
1
2
  import type { CountryCode } from "./countries.ts";
2
3
  import type { Digit } from "./types.ts";
3
4
  import { warn } from "./warn.ts";
@@ -300,8 +301,12 @@ export interface PostalAddress {
300
301
  readonly street: string;
301
302
  /** Town or city, in its own language: `"Roma"`. */
302
303
  readonly locality: string;
303
- /** Postal or ZIP code, as written locally. */
304
- readonly postalCode: string;
304
+ /**
305
+ * Postal or ZIP code, as written locally. None where the country has no
306
+ * postal codes, as in the UAE: left out rather than sent empty, like
307
+ * `region`.
308
+ */
309
+ readonly postalCode?: string;
305
310
  /**
306
311
  * The country, as its two-letter code: `"IT"`, not `"Italy"`.
307
312
  *
@@ -313,3 +318,49 @@ export interface PostalAddress {
313
318
  /** State, province or region, where the country uses one. */
314
319
  readonly region?: string;
315
320
  }
321
+
322
+ /**
323
+ * The record written out in its country's postal order, one string per line:
324
+ * `["Via Nazionale 100", "00184 Roma"]`, `["1 Example Ave", "Austin, TX
325
+ * 78701"]`.
326
+ *
327
+ * Still the canonical address — `Roma`, on every page — so it suits a list of
328
+ * venues or an envelope, not a sentence; a page that phrases the address for
329
+ * its reader writes it in the catalog, like the rest of its copy.
330
+ *
331
+ * The order is Google's address metadata, converted once into
332
+ * `addressLayout`, not a rule per country written here. A field the address does not have prints
333
+ * nothing, and its punctuation goes with it, because each field carries its
334
+ * own: no postal code, no `CH-`. Letter case is the caller's: some countries
335
+ * want the town in capitals on an envelope, which is not how a page should
336
+ * print it.
337
+ */
338
+ export function addressLines(address: PostalAddress): readonly string[] {
339
+ const lines: string[] = [];
340
+ for (const parts of addressLayout(address.country)) {
341
+ let line = "";
342
+ for (const part of parts) {
343
+ if ("text" in part) {
344
+ line += part.text;
345
+ continue;
346
+ }
347
+ const value = address[part.field]?.trim();
348
+ if (!value) continue;
349
+ if (line !== "") line += part.separator ?? "";
350
+ line += (part.prefix ?? "") + value;
351
+ }
352
+ if (line !== "") lines.push(line);
353
+ }
354
+ return lines;
355
+ }
356
+
357
+ /**
358
+ * `addressLines` on one line, for where there is room for only one: a venue
359
+ * card, an `llms.txt` entry.
360
+ */
361
+ export function addressLine(
362
+ address: PostalAddress,
363
+ separator: string = ", "
364
+ ): string {
365
+ return addressLines(address).join(separator);
366
+ }
package/src/index.ts CHANGED
@@ -51,6 +51,8 @@ export type {
51
51
  SiteConfigShape,
52
52
  } from "./config.ts";
53
53
  export {
54
+ addressLine,
55
+ addressLines,
54
56
  assertCoordinates,
55
57
  type CallingCode,
56
58
  type Coordinates,
@@ -37,7 +37,9 @@ export function postalAddress(address: PostalAddress): JsonLdNode {
37
37
  "@type": "PostalAddress",
38
38
  streetAddress: address.street,
39
39
  addressLocality: address.locality,
40
- postalCode: address.postalCode,
40
+ ...(address.postalCode === undefined
41
+ ? {}
42
+ : { postalCode: address.postalCode }),
41
43
  addressCountry: address.country,
42
44
  ...(address.region === undefined
43
45
  ? {}