@diagrammo/dgmo 0.46.0 → 0.48.0

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.
Files changed (534) hide show
  1. package/dist/advanced.d.ts +3 -855
  2. package/dist/advanced.js +541 -70107
  3. package/dist/analyzer-UYKBXAKR.js +15 -0
  4. package/dist/auto.js +1 -1
  5. package/dist/auto.mjs +1 -1
  6. package/dist/block.js +83 -69417
  7. package/dist/calculator-LDYQCDDY.js +11 -0
  8. package/dist/charts-d3-QOZHKTCT.js +25 -0
  9. package/dist/chunk-2DXMZRWJ.js +800 -0
  10. package/dist/chunk-2LACINWN.js +786 -0
  11. package/dist/chunk-33LCUH4H.js +893 -0
  12. package/dist/chunk-3CIOJ5YJ.js +35 -0
  13. package/dist/chunk-3HDBAXF5.js +86 -0
  14. package/dist/chunk-3JLKYGBQ.js +290 -0
  15. package/dist/chunk-3MQQNXPD.js +415 -0
  16. package/dist/chunk-3R5POCTZ.js +967 -0
  17. package/dist/chunk-3SMGIJDD.js +112 -0
  18. package/dist/chunk-44CM72SQ.js +409 -0
  19. package/dist/chunk-4GRI7MEB.js +1604 -0
  20. package/dist/chunk-4OHACBGX.js +606 -0
  21. package/dist/chunk-5DH3KYFH.js +21 -0
  22. package/dist/chunk-5MLYVTYM.js +76 -0
  23. package/dist/chunk-5QXL4NYS.js +553 -0
  24. package/dist/chunk-5WRI5ZAA.js +31 -0
  25. package/dist/chunk-6EK55ART.js +117 -0
  26. package/dist/chunk-6FZYCKXB.js +2065 -0
  27. package/dist/chunk-6HO5ZTXK.js +277 -0
  28. package/dist/chunk-6KFWC4CO.js +7 -0
  29. package/dist/chunk-6MTWIQVO.js +121 -0
  30. package/dist/chunk-6NYWYNBI.js +329 -0
  31. package/dist/chunk-6SM5NJQH.js +620 -0
  32. package/dist/chunk-725LDASD.js +422 -0
  33. package/dist/chunk-76OWZ3BA.js +130 -0
  34. package/{src/block/parser.ts → dist/chunk-7JEGTRLD.js} +100 -189
  35. package/dist/chunk-A44USUV7.js +923 -0
  36. package/dist/chunk-A4GOY3PE.js +25 -0
  37. package/dist/chunk-A4VFQH5G.js +251 -0
  38. package/dist/chunk-AELOBFW5.js +375 -0
  39. package/dist/chunk-AN2MOO5T.js +522 -0
  40. package/dist/chunk-BMR4CIEO.js +41 -0
  41. package/dist/chunk-BS7SV7C3.js +51 -0
  42. package/dist/chunk-BUPDIHML.js +613 -0
  43. package/dist/chunk-BUVJFRPY.js +887 -0
  44. package/dist/chunk-C3Z4DX3V.js +112 -0
  45. package/dist/chunk-CI2G3OEB.js +1818 -0
  46. package/dist/chunk-CKNVO24D.js +54 -0
  47. package/dist/chunk-CUVHQHZ7.js +391 -0
  48. package/dist/chunk-CXK4DZH3.js +114 -0
  49. package/dist/{editor.cjs → chunk-DKEPS5LH.js} +14 -225
  50. package/dist/chunk-DU7W62N5.js +487 -0
  51. package/dist/chunk-DZAQBW35.js +751 -0
  52. package/dist/chunk-EB6FPQUP.js +316 -0
  53. package/dist/chunk-EMS2SK26.js +376 -0
  54. package/dist/chunk-ERLBPDGU.js +510 -0
  55. package/dist/chunk-EVCVDTXE.js +732 -0
  56. package/dist/chunk-FCIIHT24.js +384 -0
  57. package/dist/chunk-FFUN5D66.js +97 -0
  58. package/dist/chunk-FITOLXSX.js +714 -0
  59. package/dist/chunk-FMDSF3OV.js +68 -0
  60. package/dist/chunk-FNB3PNGF.js +410 -0
  61. package/dist/chunk-G2H74XJZ.js +2104 -0
  62. package/dist/chunk-GMR5MFHJ.js +339 -0
  63. package/dist/chunk-H6PI3KJT.js +305 -0
  64. package/dist/chunk-I46X5VUA.js +59 -0
  65. package/dist/chunk-I5IGB56C.js +255 -0
  66. package/dist/chunk-IH6ZMVQ5.js +3950 -0
  67. package/dist/chunk-IJZFN3HM.js +463 -0
  68. package/dist/chunk-IPWPWM6R.js +3079 -0
  69. package/dist/chunk-IYULMRBE.js +23 -0
  70. package/dist/chunk-IZMRON6C.js +167 -0
  71. package/dist/chunk-J5FIXQRT.js +813 -0
  72. package/dist/chunk-JED6ZDEE.js +73 -0
  73. package/dist/chunk-JEUJOX5X.js +136 -0
  74. package/dist/chunk-JM4LEPY3.js +436 -0
  75. package/dist/chunk-L3UHP6TD.js +101 -0
  76. package/dist/chunk-L66GEFB6.js +524 -0
  77. package/dist/chunk-LIOHW36Z.js +469 -0
  78. package/dist/chunk-LP7EC4MI.js +16 -0
  79. package/dist/chunk-MITI6NOL.js +548 -0
  80. package/dist/chunk-MO5YHHMQ.js +168 -0
  81. package/dist/chunk-MZSBFAPJ.js +885 -0
  82. package/dist/chunk-NNAAJVBU.js +9 -0
  83. package/dist/chunk-NOTA6TEI.js +47 -0
  84. package/dist/chunk-NURGB5Z4.js +335 -0
  85. package/dist/chunk-NX4EQS3U.js +1062 -0
  86. package/dist/chunk-NXMESY4J.js +1 -0
  87. package/dist/chunk-OJR3MQYV.js +1458 -0
  88. package/dist/chunk-OURHMODR.js +454 -0
  89. package/dist/chunk-P235SVAH.js +1057 -0
  90. package/dist/chunk-P4RH4OSP.js +1038 -0
  91. package/dist/chunk-P6T66CCZ.js +226 -0
  92. package/dist/chunk-PBAHEPLQ.js +223 -0
  93. package/dist/chunk-PBHE5UG6.js +1530 -0
  94. package/dist/chunk-PDKUANDZ.js +35 -0
  95. package/dist/chunk-PWIHZ6NJ.js +296 -0
  96. package/{src/c4/layout.ts → dist/chunk-QEBUY5NY.js} +384 -1140
  97. package/dist/chunk-QLCCJEJI.js +253 -0
  98. package/dist/chunk-QOOUC2J5.js +52 -0
  99. package/dist/chunk-QYTPPDAO.js +402 -0
  100. package/dist/chunk-R3XI2VC7.js +801 -0
  101. package/dist/chunk-RCERWOJR.js +154 -0
  102. package/dist/chunk-RJE5JTVK.js +362 -0
  103. package/dist/chunk-RTLBSQ3G.js +1155 -0
  104. package/dist/chunk-RUNNI6VE.js +448 -0
  105. package/dist/chunk-RVMRZAPB.js +354 -0
  106. package/dist/chunk-RYEGFMUO.js +955 -0
  107. package/dist/chunk-RZCF2VVM.js +528 -0
  108. package/dist/chunk-SML67VPV.js +1025 -0
  109. package/dist/chunk-SQ7IY53Q.js +44 -0
  110. package/dist/chunk-SQAG3UXH.js +161 -0
  111. package/dist/chunk-SRTU6HMQ.js +1851 -0
  112. package/dist/chunk-T2OE3YQB.js +550 -0
  113. package/dist/chunk-T3UBLACL.js +299 -0
  114. package/dist/chunk-TLVIHFRO.js +296 -0
  115. package/dist/chunk-TYS6CSS5.js +52 -0
  116. package/dist/chunk-U6EQI3U6.js +130 -0
  117. package/dist/chunk-UJ4FJLBF.js +86 -0
  118. package/dist/chunk-ULOEATBZ.js +347 -0
  119. package/dist/chunk-UNCCZ3NU.js +127 -0
  120. package/dist/chunk-USZW2HHU.js +197 -0
  121. package/dist/chunk-UWN33VNX.js +1610 -0
  122. package/dist/chunk-UZ7EZW3B.js +40 -0
  123. package/dist/chunk-VOKGH627.js +37 -0
  124. package/dist/chunk-WBRZJOEC.js +204 -0
  125. package/dist/chunk-WCSOJFU3.js +380 -0
  126. package/dist/chunk-WCT3NYCV.js +160 -0
  127. package/dist/chunk-WVAJFAIA.js +529 -0
  128. package/dist/chunk-X5LCYCL5.js +394 -0
  129. package/dist/chunk-XAKEUMEJ.js +1256 -0
  130. package/dist/chunk-Y2ZE3MSG.js +590 -0
  131. package/dist/chunk-Y3J7VR52.js +646 -0
  132. package/dist/chunk-Y5QGVTVC.js +33 -0
  133. package/dist/chunk-YAA6IID3.js +548 -0
  134. package/dist/chunk-YGJTDXVR.js +692 -0
  135. package/{src/utils/time-ticks.ts → dist/chunk-YRYKTYMQ.js} +45 -112
  136. package/dist/chunk-YYVTBZZC.js +61 -0
  137. package/dist/chunk-Z5CGSFJB.js +647 -0
  138. package/{src/gantt/calculator.ts → dist/chunk-Z72XRXD4.js} +236 -428
  139. package/dist/chunk-ZGR7ULFJ.js +978 -0
  140. package/dist/chunk-ZJEL4H2R.js +851 -0
  141. package/dist/chunk-ZO27FGJS.js +554 -0
  142. package/dist/collapse-CLPJD2RL.js +11 -0
  143. package/dist/collapse-SLINJQW6.js +9 -0
  144. package/dist/collapse-ZWUFFCK2.js +9 -0
  145. package/dist/compute-G55RG53N.js +8 -0
  146. package/dist/dgmo-router-DZCIDUO5.js +73 -0
  147. package/dist/dimensions-J3FHIMJ6.js +22 -0
  148. package/dist/element.js +1 -1
  149. package/dist/element.mjs +1 -1
  150. package/dist/flowchart-parser-ZNSKIBVR.js +21 -0
  151. package/dist/flowchart-renderer-ZAEOL4DX.js +35 -0
  152. package/dist/index.d.ts +3 -469
  153. package/dist/index.js +137 -69685
  154. package/dist/layout-46N4JJYK.js +17 -0
  155. package/dist/layout-5RA6QWR6.js +26 -0
  156. package/dist/layout-5T6RH5M6.js +14 -0
  157. package/dist/layout-7QXCNOVS.js +13 -0
  158. package/dist/layout-EE6GE46Y.js +17 -0
  159. package/dist/layout-G23F7LDH.js +9 -0
  160. package/dist/layout-HRBZPFCN.js +23 -0
  161. package/dist/layout-HWDSRX5E.js +20 -0
  162. package/dist/layout-HYZPWAK5.js +18 -0
  163. package/dist/layout-N76NR4ZO.js +19 -0
  164. package/dist/layout-OB3RI63S.js +14 -0
  165. package/dist/layout-S2QBYSK2.js +15 -0
  166. package/dist/layout-UM5F4DJB.js +15 -0
  167. package/dist/layout-search-OYV77HHS.js +1655 -0
  168. package/dist/load-data-EHDDZ5ZS.js +8 -0
  169. package/dist/palettes-PRJ2RI6D.js +56 -0
  170. package/dist/parser-2MFP2H2E.js +11 -0
  171. package/dist/parser-3STE62GB.js +21 -0
  172. package/dist/parser-4FL2CMDG.js +18 -0
  173. package/dist/parser-4OGOBJJ4.js +13 -0
  174. package/dist/parser-6BTRLKRZ.js +15 -0
  175. package/dist/parser-AQETEVRW.js +21 -0
  176. package/dist/parser-CFZD5IBL.js +12 -0
  177. package/dist/parser-FUSKFGZN.js +16 -0
  178. package/dist/parser-I6KXEYFK.js +13 -0
  179. package/dist/parser-IXSZZQHY.js +18 -0
  180. package/dist/parser-JAL2HD4I.js +13 -0
  181. package/dist/parser-KNSQSKQO.js +20 -0
  182. package/dist/parser-N5O4IHUN.js +13 -0
  183. package/dist/parser-NGU3ETO6.js +16 -0
  184. package/dist/parser-NRAI6O25.js +19 -0
  185. package/dist/parser-O5J4VBII.js +11 -0
  186. package/dist/parser-OV7NOBW6.js +21 -0
  187. package/dist/parser-PQ3POX45.js +23 -0
  188. package/dist/parser-TFZFBVWN.js +11 -0
  189. package/dist/parser-TRTHDFPP.js +18 -0
  190. package/dist/parser-VQP2UJ6W.js +11 -0
  191. package/dist/parser-XDVELHRK.js +13 -0
  192. package/dist/parser-XKRP7QKP.js +11 -0
  193. package/dist/parser-Y6WTBU76.js +14 -0
  194. package/dist/parser-ZSDPZYQP.js +12 -0
  195. package/dist/renderer-2XQQQ6QR.js +28 -0
  196. package/dist/renderer-5NDVNDAZ.js +26 -0
  197. package/dist/renderer-6EEZHMH4.js +26 -0
  198. package/dist/renderer-6W5462RB.js +43 -0
  199. package/dist/renderer-7MVLWWQD.js +22 -0
  200. package/dist/renderer-AWWAF6XD.js +33 -0
  201. package/dist/renderer-BYCNMBF7.js +34 -0
  202. package/dist/renderer-CLJHQPBJ.js +29 -0
  203. package/dist/renderer-GHPZMFU2.js +15 -0
  204. package/dist/renderer-JGBKUCZD.js +16 -0
  205. package/dist/renderer-KQB7UGDS.js +19 -0
  206. package/dist/renderer-PGX2TKVS.js +24 -0
  207. package/dist/renderer-QIWVHWZO.js +15 -0
  208. package/dist/renderer-RPOA5YPZ.js +35 -0
  209. package/dist/renderer-SMIBCRVB.js +24 -0
  210. package/dist/renderer-T7YKC5VP.js +28 -0
  211. package/dist/renderer-TOCOVLHS.js +37 -0
  212. package/dist/renderer-UWA5XS74.js +39 -0
  213. package/dist/renderer-VACVQYML.js +25 -0
  214. package/dist/renderer-W4M6QLIJ.js +37 -0
  215. package/dist/renderer-WZRREQ2P.js +23 -0
  216. package/dist/renderer-YY2OSF6E.js +27 -0
  217. package/dist/renderer-Z7I3W2PP.js +24 -0
  218. package/dist/renderer-ZGNTUWC4.js +22 -0
  219. package/dist/renderer-ZTR3DER3.js +19 -0
  220. package/dist/renderer-radial-C32GPJQ7.js +21 -0
  221. package/dist/resolver-NOAA7ID7.js +13 -0
  222. package/dist/state-collapse-HCGD6IVJ.js +8 -0
  223. package/dist/state-parser-Z2V23ATR.js +18 -0
  224. package/dist/state-renderer-RFYHSP4K.js +34 -0
  225. package/dist/themes-1-CuKpeH.d.ts +908 -0
  226. package/package.json +19 -70
  227. package/dist/advanced.cjs +0 -71648
  228. package/dist/advanced.d.cts +0 -6571
  229. package/dist/auto.cjs +0 -70648
  230. package/dist/auto.d.cts +0 -54
  231. package/dist/block.cjs +0 -70208
  232. package/dist/block.d.cts +0 -115
  233. package/dist/editor.d.cts +0 -26
  234. package/dist/element.cjs +0 -70530
  235. package/dist/element.d.cts +0 -62
  236. package/dist/highlight.cjs +0 -852
  237. package/dist/highlight.d.cts +0 -49
  238. package/dist/index.cjs +0 -70491
  239. package/dist/index.d.cts +0 -626
  240. package/dist/pert.cjs +0 -325
  241. package/dist/pert.d.cts +0 -559
  242. package/src/advanced.ts +0 -873
  243. package/src/arc/parser.ts +0 -15
  244. package/src/arc/renderer.ts +0 -633
  245. package/src/auto/index.ts +0 -621
  246. package/src/auto/shared.ts +0 -370
  247. package/src/auto/styles.ts +0 -73
  248. package/src/block/layout.ts +0 -198
  249. package/src/block/renderer.ts +0 -484
  250. package/src/block/types.ts +0 -67
  251. package/src/boxes-and-lines/collapse.ts +0 -96
  252. package/src/boxes-and-lines/focus.ts +0 -257
  253. package/src/boxes-and-lines/label-placement.ts +0 -339
  254. package/src/boxes-and-lines/layout-grouped.ts +0 -912
  255. package/src/boxes-and-lines/layout-layered.ts +0 -722
  256. package/src/boxes-and-lines/layout-search.ts +0 -1678
  257. package/src/boxes-and-lines/layout.ts +0 -398
  258. package/src/boxes-and-lines/parser.ts +0 -1181
  259. package/src/boxes-and-lines/renderer.ts +0 -1465
  260. package/src/boxes-and-lines/types.ts +0 -66
  261. package/src/c4/parser.ts +0 -1158
  262. package/src/c4/renderer.ts +0 -2008
  263. package/src/c4/types.ts +0 -89
  264. package/src/chart-type-registry.ts +0 -529
  265. package/src/chart-types.ts +0 -225
  266. package/src/chart.ts +0 -685
  267. package/src/charts-d3/bar.ts +0 -338
  268. package/src/charts-d3/chord.ts +0 -140
  269. package/src/charts-d3/function.ts +0 -147
  270. package/src/charts-d3/funnel.ts +0 -104
  271. package/src/charts-d3/heatmap.ts +0 -145
  272. package/src/charts-d3/index.ts +0 -337
  273. package/src/charts-d3/interactions.ts +0 -477
  274. package/src/charts-d3/line.ts +0 -344
  275. package/src/charts-d3/mount.ts +0 -151
  276. package/src/charts-d3/pie.ts +0 -117
  277. package/src/charts-d3/polar.ts +0 -89
  278. package/src/charts-d3/radar.ts +0 -0
  279. package/src/charts-d3/sankey.ts +0 -0
  280. package/src/charts-d3/scatter.ts +0 -185
  281. package/src/charts-d3/shared.ts +0 -208
  282. package/src/class/layout.ts +0 -326
  283. package/src/class/parser.ts +0 -635
  284. package/src/class/renderer.ts +0 -793
  285. package/src/class/types.ts +0 -58
  286. package/src/cli-banner.ts +0 -106
  287. package/src/cli.ts +0 -1080
  288. package/src/colors-diagnostics.ts +0 -35
  289. package/src/colors.ts +0 -379
  290. package/src/completion-types.ts +0 -27
  291. package/src/completion.ts +0 -2668
  292. package/src/cycle/layout.ts +0 -1026
  293. package/src/cycle/parser.ts +0 -387
  294. package/src/cycle/renderer.ts +0 -653
  295. package/src/cycle/types.ts +0 -101
  296. package/src/d3.ts +0 -1434
  297. package/src/data-chart-parser.ts +0 -1007
  298. package/src/dgmo-router.ts +0 -296
  299. package/src/diagnostics-registry.ts +0 -108
  300. package/src/diagnostics.ts +0 -0
  301. package/src/dimensions.ts +0 -20
  302. package/src/directives-registry.ts +0 -396
  303. package/src/editor/dgmo.grammar +0 -76
  304. package/src/editor/dgmo.grammar.d.ts +0 -2
  305. package/src/editor/dgmo.grammar.js +0 -18
  306. package/src/editor/dgmo.grammar.terms.d.ts +0 -5
  307. package/src/editor/dgmo.grammar.terms.js +0 -35
  308. package/src/editor/highlight-api.ts +0 -596
  309. package/src/editor/highlight.ts +0 -36
  310. package/src/editor/index.ts +0 -27
  311. package/src/editor/keywords.ts +0 -128
  312. package/src/editor/tokens.ts +0 -30
  313. package/src/element/index.ts +0 -432
  314. package/src/embed/css.ts +0 -312
  315. package/src/embed/escape.ts +0 -15
  316. package/src/embed/index.ts +0 -280
  317. package/src/er/classify.ts +0 -228
  318. package/src/er/layout.ts +0 -530
  319. package/src/er/parser.ts +0 -750
  320. package/src/er/renderer.ts +0 -725
  321. package/src/er/types.ts +0 -52
  322. package/src/error-card.ts +0 -269
  323. package/src/event-line/diagnostics.ts +0 -117
  324. package/src/event-line/parser.ts +0 -643
  325. package/src/event-line/renderer.ts +0 -1742
  326. package/src/event-line/types.ts +0 -74
  327. package/src/fonts.ts +0 -3
  328. package/src/gantt/parser.ts +0 -1641
  329. package/src/gantt/renderer.ts +0 -3787
  330. package/src/gantt/resolver.ts +0 -172
  331. package/src/gantt/types.ts +0 -206
  332. package/src/graph/diagnostics.ts +0 -64
  333. package/src/graph/edge-spline.ts +0 -29
  334. package/src/graph/flowchart-parser.ts +0 -759
  335. package/src/graph/flowchart-renderer.ts +0 -817
  336. package/src/graph/layout.ts +0 -472
  337. package/src/graph/notes.ts +0 -21
  338. package/src/graph/state-collapse.ts +0 -78
  339. package/src/graph/state-parser.ts +0 -574
  340. package/src/graph/state-renderer.ts +0 -653
  341. package/src/graph/types.ts +0 -65
  342. package/src/index.ts +0 -261
  343. package/src/infra/compute.ts +0 -1385
  344. package/src/infra/layout.ts +0 -888
  345. package/src/infra/parser.ts +0 -1234
  346. package/src/infra/renderer.ts +0 -2534
  347. package/src/infra/roles.ts +0 -75
  348. package/src/infra/types.ts +0 -216
  349. package/src/infra/validation.ts +0 -201
  350. package/src/journey-map/layout.ts +0 -490
  351. package/src/journey-map/parser.ts +0 -557
  352. package/src/journey-map/renderer.ts +0 -1580
  353. package/src/journey-map/types.ts +0 -47
  354. package/src/kanban/mutations.ts +0 -175
  355. package/src/kanban/parser.ts +0 -509
  356. package/src/kanban/renderer.ts +0 -1449
  357. package/src/kanban/types.ts +0 -39
  358. package/src/label-layout.ts +0 -368
  359. package/src/map/colorize.ts +0 -54
  360. package/src/map/completion.ts +0 -316
  361. package/src/map/context-labels.ts +0 -609
  362. package/src/map/data/PROVENANCE.json +0 -1
  363. package/src/map/data/README.md +0 -40
  364. package/src/map/data/airport-collisions.json +0 -1
  365. package/src/map/data/airports.json +0 -1
  366. package/src/map/data/gazetteer.json +0 -1
  367. package/src/map/data/lakes.json +0 -1
  368. package/src/map/data/mountain-ranges.json +0 -1
  369. package/src/map/data/na-lakes.json +0 -1
  370. package/src/map/data/na-land.json +0 -1
  371. package/src/map/data/region-names.json +0 -1
  372. package/src/map/data/rivers.json +0 -1
  373. package/src/map/data/types.ts +0 -121
  374. package/src/map/data/us-states.json +0 -1
  375. package/src/map/data/water-bodies.json +0 -1
  376. package/src/map/data/world-coarse.json +0 -1
  377. package/src/map/data/world-detail.json +0 -1
  378. package/src/map/diagnostics.ts +0 -134
  379. package/src/map/dimensions.ts +0 -150
  380. package/src/map/geo-query.ts +0 -295
  381. package/src/map/geo.ts +0 -438
  382. package/src/map/invert.ts +0 -111
  383. package/src/map/layout.ts +0 -5069
  384. package/src/map/legend-band.ts +0 -127
  385. package/src/map/load-data.ts +0 -186
  386. package/src/map/parser.ts +0 -783
  387. package/src/map/renderer.ts +0 -1445
  388. package/src/map/resolved-types.ts +0 -160
  389. package/src/map/resolver.ts +0 -1146
  390. package/src/map/types.ts +0 -194
  391. package/src/mindmap/collapse.ts +0 -67
  392. package/src/mindmap/layout.ts +0 -649
  393. package/src/mindmap/parser.ts +0 -430
  394. package/src/mindmap/renderer.ts +0 -637
  395. package/src/mindmap/text-wrap.ts +0 -218
  396. package/src/mindmap/types.ts +0 -56
  397. package/src/org/collapse.ts +0 -151
  398. package/src/org/layout.ts +0 -1262
  399. package/src/org/parser.ts +0 -519
  400. package/src/org/renderer.ts +0 -866
  401. package/src/org/resolver.ts +0 -636
  402. package/src/palettes/atlas.ts +0 -77
  403. package/src/palettes/blueprint.ts +0 -73
  404. package/src/palettes/catppuccin.ts +0 -84
  405. package/src/palettes/color-utils.ts +0 -443
  406. package/src/palettes/index.ts +0 -67
  407. package/src/palettes/nord.ts +0 -67
  408. package/src/palettes/registry.ts +0 -114
  409. package/src/palettes/slate.ts +0 -73
  410. package/src/palettes/tidewater.ts +0 -73
  411. package/src/palettes/tokyo-night.ts +0 -86
  412. package/src/palettes/types.ts +0 -88
  413. package/src/pert/analyzer.ts +0 -1183
  414. package/src/pert/diagnostics.ts +0 -110
  415. package/src/pert/index.ts +0 -43
  416. package/src/pert/internal.ts +0 -252
  417. package/src/pert/layout.ts +0 -971
  418. package/src/pert/monte-carlo.ts +0 -446
  419. package/src/pert/parser.ts +0 -1606
  420. package/src/pert/renderer.ts +0 -4009
  421. package/src/pert/share-normalize.ts +0 -62
  422. package/src/pert/types.ts +0 -604
  423. package/src/pyramid/parser.ts +0 -185
  424. package/src/pyramid/renderer.ts +0 -768
  425. package/src/pyramid/types.ts +0 -28
  426. package/src/quadrant/parser.ts +0 -18
  427. package/src/quadrant/renderer.ts +0 -638
  428. package/src/raci/diagnostics.ts +0 -213
  429. package/src/raci/index.ts +0 -23
  430. package/src/raci/mutations.ts +0 -321
  431. package/src/raci/parser.ts +0 -699
  432. package/src/raci/renderer.ts +0 -1627
  433. package/src/raci/types.ts +0 -82
  434. package/src/raci/variants.ts +0 -320
  435. package/src/render.ts +0 -268
  436. package/src/ring/parser.ts +0 -212
  437. package/src/ring/renderer.ts +0 -490
  438. package/src/ring/types.ts +0 -27
  439. package/src/sequence/collapse.ts +0 -174
  440. package/src/sequence/parser.ts +0 -1596
  441. package/src/sequence/participant-inference.ts +0 -152
  442. package/src/sequence/renderer.ts +0 -3168
  443. package/src/sequence/tag-resolution.ts +0 -164
  444. package/src/sharing.ts +0 -211
  445. package/src/sitemap/collapse.ts +0 -100
  446. package/src/sitemap/layout.ts +0 -925
  447. package/src/sitemap/parser.ts +0 -755
  448. package/src/sitemap/renderer.ts +0 -828
  449. package/src/sitemap/types.ts +0 -42
  450. package/src/slope/parser.ts +0 -18
  451. package/src/slope/renderer.ts +0 -378
  452. package/src/swimlane/diagnostics.ts +0 -94
  453. package/src/swimlane/layout.ts +0 -597
  454. package/src/swimlane/parser.ts +0 -613
  455. package/src/swimlane/renderer.ts +0 -681
  456. package/src/swimlane/types.ts +0 -151
  457. package/src/tech-radar/interactive.ts +0 -1129
  458. package/src/tech-radar/layout.ts +0 -197
  459. package/src/tech-radar/parser.ts +0 -436
  460. package/src/tech-radar/renderer.ts +0 -1239
  461. package/src/tech-radar/shared.ts +0 -151
  462. package/src/tech-radar/types.ts +0 -86
  463. package/src/themes.ts +0 -22
  464. package/src/timeline/parser.ts +0 -472
  465. package/src/timeline/renderer.ts +0 -3024
  466. package/src/timeline/types.ts +0 -33
  467. package/src/timeline/viz-parser.ts +0 -21
  468. package/src/treemap/diagnostics.ts +0 -62
  469. package/src/treemap/layout-radial.ts +0 -233
  470. package/src/treemap/layout.ts +0 -189
  471. package/src/treemap/parser.ts +0 -463
  472. package/src/treemap/renderer-radial.ts +0 -438
  473. package/src/treemap/renderer.ts +0 -572
  474. package/src/treemap/treemap-shared.ts +0 -309
  475. package/src/treemap/types.ts +0 -60
  476. package/src/utils/arrow-markers.ts +0 -51
  477. package/src/utils/arrows-diagnostics.ts +0 -62
  478. package/src/utils/arrows.ts +0 -227
  479. package/src/utils/brand.ts +0 -61
  480. package/src/utils/card.ts +0 -183
  481. package/src/utils/collapse-engine/tree-edges.ts +0 -115
  482. package/src/utils/collapse-engine/tree.ts +0 -117
  483. package/src/utils/d3-helpers.ts +0 -219
  484. package/src/utils/d3-types.ts +0 -9
  485. package/src/utils/description-helpers.ts +0 -38
  486. package/src/utils/duration.ts +0 -312
  487. package/src/utils/export-container.ts +0 -41
  488. package/src/utils/expr-eval.ts +0 -27
  489. package/src/utils/fit-canvas.ts +0 -64
  490. package/src/utils/hover-styles.ts +0 -673
  491. package/src/utils/inline-markdown.ts +0 -96
  492. package/src/utils/legend-constants.ts +0 -48
  493. package/src/utils/legend-d3.ts +0 -618
  494. package/src/utils/legend-integration.ts +0 -107
  495. package/src/utils/legend-layout.ts +0 -776
  496. package/src/utils/legend-svg.ts +0 -214
  497. package/src/utils/legend-types.ts +0 -272
  498. package/src/utils/name-normalize.ts +0 -163
  499. package/src/utils/note-box/constants.ts +0 -25
  500. package/src/utils/note-box/index.ts +0 -11
  501. package/src/utils/note-box/metrics.ts +0 -90
  502. package/src/utils/note-box/svg.ts +0 -331
  503. package/src/utils/notes/bounds.ts +0 -30
  504. package/src/utils/notes/build.ts +0 -131
  505. package/src/utils/notes/index.ts +0 -18
  506. package/src/utils/notes/model.ts +0 -19
  507. package/src/utils/notes/parse.ts +0 -131
  508. package/src/utils/notes/place.ts +0 -177
  509. package/src/utils/notes/resolve.ts +0 -88
  510. package/src/utils/number-format.ts +0 -36
  511. package/src/utils/parsing.ts +0 -953
  512. package/src/utils/reserved-key-registry.ts +0 -239
  513. package/src/utils/safe-href.ts +0 -38
  514. package/src/utils/scaling.ts +0 -97
  515. package/src/utils/svg-embed.ts +0 -251
  516. package/src/utils/tag-groups.ts +0 -607
  517. package/src/utils/text-measure.ts +0 -122
  518. package/src/utils/title-constants.ts +0 -24
  519. package/src/utils/visual-conventions.ts +0 -61
  520. package/src/utils/wrapped-desc.ts +0 -87
  521. package/src/venn/parser.ts +0 -18
  522. package/src/venn/renderer.ts +0 -1047
  523. package/src/version-control/diagnostics.ts +0 -67
  524. package/src/version-control/parser.ts +0 -544
  525. package/src/version-control/renderer.ts +0 -724
  526. package/src/version-control/types.ts +0 -100
  527. package/src/visualizations/parse.ts +0 -1560
  528. package/src/visualizations/types.ts +0 -262
  529. package/src/wireframe/layout.ts +0 -479
  530. package/src/wireframe/parser.ts +0 -1114
  531. package/src/wireframe/renderer.ts +0 -1318
  532. package/src/wireframe/types.ts +0 -110
  533. package/src/wordcloud/parser.ts +0 -18
  534. package/src/wordcloud/renderer.ts +0 -357
@@ -1,1146 +0,0 @@
1
- // Resolver (step 3): ParsedMap + MapData → ResolvedMap. SYNCHRONOUS + PURE
2
- // (deterministic, no I/O — the async asset load lives in ./load-data.ts; DI).
3
- // Resolves region names → ISO geometry, POI/endpoint names → coords, infers the
4
- // basemap/scope/extent/projection, and emits resolved-identity diagnostics. See
5
- // §24B.2/.8/.10 and the tech-spec adversarial resolutions R1–R19.
6
- import { emit, formatDgmoError, suggest } from '../diagnostics';
7
- import type { DgmoError } from '../diagnostics';
8
- import { MAP_DX } from './diagnostics';
9
- import type { Writable } from '../utils/brand';
10
- import type { ParsedMap, PoiPos } from './types';
11
- import type {
12
- MapData,
13
- ResolvedMap,
14
- ResolvedRegion,
15
- ResolvedPoi,
16
- ResolvedEdge,
17
- ResolvedRoute,
18
- ResolvedRouteLeg,
19
- ProjectionFamily,
20
- GeoExtent,
21
- } from './resolved-types';
22
- import {
23
- featureIndex,
24
- featureBbox,
25
- featureBboxPrimary,
26
- unionExtent,
27
- fold,
28
- decodeFeatures,
29
- regionAt,
30
- } from './geo';
31
-
32
- /** Discriminated result of a gazetteer name lookup (#5): `defer` is "ambiguous,
33
- * retry in pass B with inferred scope" — distinct from `miss` (errored, drop) so
34
- * pass-A deferral never has to infer state from unrelated same-line diagnostics. */
35
- type LookupResult =
36
- | { kind: 'ok'; lat: number; lon: number; iso: string }
37
- | { kind: 'defer' }
38
- | { kind: 'miss' };
39
-
40
- // Projection / tier thresholds (degrees of span) — tunable (R10).
41
- const WORLD_SPAN = 90;
42
- // Mercator is used for everything sub-world (tight clusters AND single-continent
43
- // regional views — a mid-latitude continent reads with its familiar conventional
44
- // shape). Two guards push back to the equirectangular world projection: a
45
- // world/multi-continent `span` (> WORLD_SPAN), or
46
- // a frame that reaches into polar latitudes (> MERCATOR_MAX_LAT) where Mercator's
47
- // sec(φ) area blow-up turns gross. Europe (≈71°N) and East Asia stay on Mercator.
48
- const MERCATOR_MAX_LAT = 80;
49
- const PAD_FRACTION = 0.05;
50
- // Region/choropleth maps frame to the NAMED countries/states; a tight 5% pad cuts
51
- // the surrounding continent right at the data edge (a Europe choropleth stops at
52
- // ~31°E, slicing off western Russia/Ukraine). A larger pad lets the neighbouring
53
- // land peek in as gray context for orientation. POI maps keep the tight pad — they
54
- // already get container-region framing + the zoom floor.
55
- const REGION_PAD_FRACTION = 0.12;
56
- // Latitude band for a snapped world view — Tierra del Fuego (≈ −55°) to northern
57
- // Russia/Canada (≈ +78°). Excludes most of Antarctica + the high Arctic so the
58
- // populated continents fill the frame rather than waste it on ice.
59
- const WORLD_LAT_SOUTH = -58;
60
- const WORLD_LAT_NORTH = 78;
61
- // Tightest zoom for a POI-only cluster: never frame narrower than this many
62
- // degrees on the longer axis. The basemap is state/country geometry only (no
63
- // counties/cities/roads), so a metro-scale frame is an empty box — clamp up to a
64
- // multi-state window that always shows coastline + a state outline or two around
65
- // the dots. A tight cluster (e.g. Bay Area cities) therefore frames as ≈ its
66
- // home state + neighbours rather than the whole nation. Tunable.
67
- const POI_ZOOM_FLOOR_DEG = 7;
68
- // POI-only container framing reveals the region(s) that CONTAIN the dots, but a
69
- // single POI near the edge of a tall/wide country (e.g. Cartagena at the north
70
- // tip of Colombia) would otherwise drag the frame to that country's far edge —
71
- // all the way to the Amazon, ~15° below the southernmost dot. Clamp the container
72
- // union so it reveals at most `containerOvershoot(cluster)` degrees of container
73
- // BEYOND the POI cluster on each side: northern Colombia stays for orientation,
74
- // the empty interior is cropped.
75
- //
76
- // For NON-US clusters the overshoot SHRINKS as the cluster grows (tuned
77
- // 2026-06-19). A tiny cluster has no context of its own, so it keeps the full MAX
78
- // (8°) — enough to reveal its whole modest container (e.g. a small European
79
- // country). A LARGE cluster already supplies its own context, so a fixed 8° just
80
- // padded a huge container with empty land (a Ukraine/Russia strike map framed
81
- // ~2.4× the cluster, a wide dead band above the dots). Linearly decaying the
82
- // overshoot to MIN (3°) tightens big clusters (~1.7× cluster) without cropping
83
- // small ones. The POI_ZOOM_FLOOR_DEG floor still guards the lower bound.
84
- //
85
- // US-ORIENTED maps are EXEMPT (keep the flat MAX): the national-vs-regional
86
- // projection gate (US_NATIONAL_LON_SPAN, albers-usa vs regional Mercator) is
87
- // calibrated against the 8°-overshoot frame span — a coast-to-Caribbean US cruise
88
- // route clears the national threshold only because the west overshoot reaches ~8°
89
- // past Denver. Shrinking it there would silently flip such maps off albers-usa.
90
- // US containers (a state, CONUS) aren't the huge-empty-container problem anyway.
91
- const CONTAINER_OVERSHOOT_MAX = 8;
92
- const CONTAINER_OVERSHOOT_MIN = 3;
93
- // Degrees of overshoot shed per degree of cluster span (larger span ⇒ less slack).
94
- const CONTAINER_OVERSHOOT_DECAY = 0.3;
95
-
96
- /** Per-cluster container overshoot (deg). US-oriented maps keep the flat MAX (the
97
- * albers-usa national gate is calibrated to it); other maps get full MAX for a
98
- * tight cluster, decaying to MIN for a large one. `span` = the cluster's larger
99
- * lon/lat extent. */
100
- function containerOvershoot(span: number, usOriented: boolean): number {
101
- if (usOriented) return CONTAINER_OVERSHOOT_MAX;
102
- return Math.max(
103
- CONTAINER_OVERSHOOT_MIN,
104
- Math.min(
105
- CONTAINER_OVERSHOOT_MAX,
106
- CONTAINER_OVERSHOOT_MAX - CONTAINER_OVERSHOOT_DECAY * span
107
- )
108
- );
109
- }
110
- // Above this longitudinal span a US POI-only extent is "national" — use the
111
- // albers-usa composite (CONUS conic + AK/HI insets) instead of regional Mercator.
112
- // CONUS spans ≈58° lon; 48° is "most of the country". Tunable.
113
- const US_NATIONAL_LON_SPAN = 48;
114
- // Sub-national US auto-zoom (map-us-subnational-zoom). A US region/choropleth map
115
- // whose authored data occupies a geographically COMPACT slice of the country
116
- // zooms to that slice (conic-equal-area, like every non-US regional view) instead
117
- // of always framing the whole nation on albers-usa. "Compact" = the raw data bbox
118
- // covers less than US_SUBNATIONAL_AREA_FRACTION of the CONUS bbox AREA. Area (not
119
- // lon-span) is used so a near-national diagonal (e.g. WA+FL) stays national while
120
- // a tight cluster (the Northeast) or a tall corridor zooms. CONUS_BBOX ≈ the
121
- // contiguous 48 (≈1416 deg²); the fraction bisects the empty gap between a
122
- // regional cluster (≲0.15) and a national map (≳0.9). Tunable.
123
- const CONUS_BBOX: GeoExtent = [
124
- [-125, 25],
125
- [-66, 49],
126
- ];
127
- const US_SUBNATIONAL_AREA_FRACTION = 0.4;
128
-
129
- /** Rectangular (lon×lat) area of a bbox in deg² — NOT d3 `geoArea` (spherical).
130
- * The gate frames with a lon/lat bbox anyway, so a planar ratio is the honest
131
- * measure of "how much of the country does this occupy". */
132
- function bboxArea(b: GeoExtent): number {
133
- return (b[1][0] - b[0][0]) * (b[1][1] - b[0][1]);
134
- }
135
-
136
- /** True when a US data extent is compact enough to zoom to (sub-national) rather
137
- * than frame the whole nation. Pure + side-effect-free (unit-tested in isolation
138
- * with no fixtures/render). Pass the RAW unpadded data-union bbox, not the padded
139
- * framing extent — the fraction is calibrated on raw data bboxes. */
140
- export function isSubNationalUsExtent(bbox: GeoExtent | null): boolean {
141
- if (!bbox) return false;
142
- return bboxArea(bbox) / bboxArea(CONUS_BBOX) < US_SUBNATIONAL_AREA_FRACTION;
143
- }
144
-
145
- // Long-form (or common-alias) country name → the folded Natural-Earth display
146
- // name actually shipped in world-coarse (#6). The NE coarse layer abbreviates a
147
- // handful of names ("Dem. Rep. Congo", "W. Sahara", …) that a user would never
148
- // type; this rescues them. ISO-code matching (featureIndex id keys) covers the
149
- // rest. Keys/values are pre-folded (lowercase, diacritics stripped).
150
- const REGION_ALIASES: Readonly<Record<string, string>> = {
151
- // Common everyday names → the Natural-Earth display name actually shipped.
152
- 'united states': 'united states of america',
153
- usa: 'united states of america',
154
- america: 'united states of america',
155
- uk: 'united kingdom',
156
- 'western sahara': 'w. sahara',
157
- 'democratic republic of the congo': 'dem. rep. congo',
158
- 'dr congo': 'dem. rep. congo',
159
- drc: 'dem. rep. congo',
160
- 'dominican republic': 'dominican rep.',
161
- 'falkland islands': 'falkland is.',
162
- 'ivory coast': "cote d'ivoire",
163
- 'central african republic': 'central african rep.',
164
- 'equatorial guinea': 'eq. guinea',
165
- 'solomon islands': 'solomon is.',
166
- 'bosnia and herzegovina': 'bosnia and herz.',
167
- 'south sudan': 's. sudan',
168
- 'north macedonia': 'macedonia',
169
- 'czech republic': 'czechia',
170
- };
171
-
172
- // US state (+ DC) postal abbreviations. A bare trailing two-letter scope token
173
- // that is one of these resolves to the US STATE `US-XX` (not the colliding
174
- // ISO 3166-1 country code — e.g. `CA` = California, not Canada) and signals US
175
- // scope (§24B.8, 2026-06-01). Non-state two-letter tokens (`CR`, `FR`) stay
176
- // country codes. Trade-off: a foreign country whose code collides with a state
177
- // (DE/IN/AR/CO/LA/PA/…) can't be picked by bare code — use the city name alone
178
- // (most-populous) or coordinates.
179
- const US_STATE_POSTAL: ReadonlySet<string> = new Set([
180
- 'AL',
181
- 'AK',
182
- 'AZ',
183
- 'AR',
184
- 'CA',
185
- 'CO',
186
- 'CT',
187
- 'DE',
188
- 'FL',
189
- 'GA',
190
- 'HI',
191
- 'ID',
192
- 'IL',
193
- 'IN',
194
- 'IA',
195
- 'KS',
196
- 'KY',
197
- 'LA',
198
- 'ME',
199
- 'MD',
200
- 'MA',
201
- 'MI',
202
- 'MN',
203
- 'MS',
204
- 'MO',
205
- 'MT',
206
- 'NE',
207
- 'NV',
208
- 'NH',
209
- 'NJ',
210
- 'NM',
211
- 'NY',
212
- 'NC',
213
- 'ND',
214
- 'OH',
215
- 'OK',
216
- 'OR',
217
- 'PA',
218
- 'RI',
219
- 'SC',
220
- 'SD',
221
- 'TN',
222
- 'TX',
223
- 'UT',
224
- 'VT',
225
- 'VA',
226
- 'WA',
227
- 'WV',
228
- 'WI',
229
- 'WY',
230
- 'DC',
231
- ]);
232
-
233
- /** A bare two-letter scope token that names a US state → its `US-XX` 3166-2 id
234
- * (`OR` → `US-OR`), else null. Case-insensitive. */
235
- function usStateFromBareScope(scope: string | undefined): string | null {
236
- if (!scope) return null;
237
- const up = scope.toUpperCase();
238
- return US_STATE_POSTAL.has(up) ? `US-${up}` : null;
239
- }
240
-
241
- /** Rough US bounding box (incl. AK across the dateline, HI, PR) for classifying
242
- * bare coordinate POIs as US-or-not when deciding `albers-usa` (#13). */
243
- function looksUS(lat: number, lon: number): boolean {
244
- if (lat < 15 || lat > 72) return false;
245
- // continental + AK + HI + Caribbean territories: lon in [-180, -64];
246
- // Aleutians wrap past the antimeridian to ~+172.
247
- return (lon >= -180 && lon <= -64) || lon >= 172;
248
- }
249
-
250
- /** Classifies a bare-coordinate POI as a North-American neighbour (vs a far-away
251
- * blocker) when deciding `albers-usa`. Used only for bare coords — named POIs
252
- * carry an ISO. This deliberately only covers what `looksUS`'s broad box does
253
- * NOT: the Atlantic-Canada strip east of −64° (Newfoundland/Labrador/Nova
254
- * Scotia, e.g. St. John's at −52.7) which falls outside `looksUS`'s
255
- * [−180, −64] longitude window. Most of Canada and all of Mexico already pass
256
- * `looksUS` (their lon is ≤ −64, lat ≥ 15) so they never reach here. The
257
- * latitude is capped at 72° (matching `looksUS`) so a high-Arctic coord is NOT
258
- * treated as a neighbour — that would both distort the conus fit and collide
259
- * with the Alaska inset bbox (`inAlaska`, lat ≥ 51 ∧ lon ≤ −129). NOTE: the
260
- * east strip can also catch the SE-Greenland coast (rare, acceptable; named
261
- * GL/DK POIs are unaffected since they classify by ISO). */
262
- function looksNorthAmericaNeighbor(lat: number, lon: number): boolean {
263
- return lat >= 14 && lat <= 72 && lon >= -141 && lon <= -52;
264
- }
265
-
266
- /** A bbox that (near-)spans the whole globe — the sentinel spherical geoBounds
267
- * returns for a malformed/missing feature. Such a box is useless for framing. */
268
- function isWholeSphere(bb: GeoExtent): boolean {
269
- return (
270
- bb[0][0] <= -179 && bb[1][0] >= 179 && bb[0][1] <= -89 && bb[1][1] >= 89
271
- );
272
- }
273
-
274
- export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
275
- const diagnostics: DgmoError[] = [...parsed.diagnostics]; // seed with parse diags (R14)
276
- // Folded tokens already flagged as "city shadows an airport code" — emit the
277
- // W_MAP_AIRPORT_SHADOWED_BY_CITY hint at most once per code (ADR-2, F9).
278
- const shadowedAirports = new Set<string>();
279
-
280
- const result: Writable<ResolvedMap> = {
281
- title: parsed.title,
282
- ...(parsed.directives.caption !== undefined && {
283
- caption: parsed.directives.caption,
284
- }),
285
- tagGroups: [...parsed.tagGroups],
286
- // Shallow-copy so the resolved model never aliases the parser's mutable
287
- // directives object (#11). NOTE: tag→region-fill COLOR binding is the
288
- // renderer's job (step 4) — the resolver only carries `tags` + `tagGroups`
289
- // through; it never resolves a tag value to a palette color (#10).
290
- directives: { ...parsed.directives },
291
- basemaps: { world: 'detail', subdivisions: [] },
292
- regions: [],
293
- pois: [],
294
- edges: [],
295
- routes: [],
296
- extent: [
297
- [-180, -85],
298
- [180, 85],
299
- ] as GeoExtent,
300
- projection: 'equirectangular',
301
- poiFrameContainers: [],
302
- diagnostics,
303
- error: parsed.error,
304
- };
305
-
306
- // Per-layer indexes (never merged — R12; coarse is the authoritative name
307
- // index, ids shared with detail — R13). LOAD-BEARING: `worldCoarse` (110m) is
308
- // NOT used for rendering anymore (the world basemap is pinned to detail/50m,
309
- // see basemaps assignment below) — it is retained solely as this name index
310
- // (featureIndex) and the dominant-landmass bbox source (featureBboxPrimary).
311
- // Do not delete it.
312
- const countryIndex = featureIndex(data.worldCoarse);
313
- const usStateIndex = featureIndex(data.usStates);
314
- const allNames = [
315
- ...[...countryIndex.values()].map((v) => v.name),
316
- ...[...usStateIndex.values()].map((v) => v.name),
317
- ];
318
-
319
- // ── locale <ISO> → country + optional subdivision (§24B.8) ──
320
- const localeRaw = parsed.directives.locale?.toUpperCase();
321
- const localeCountry = localeRaw ? localeRaw.split('-')[0] : undefined;
322
- const localeSubdivision =
323
- localeRaw && /^[A-Z]{2}-/.test(localeRaw) ? localeRaw : undefined;
324
-
325
- // ── US-scope signal (drives the country-vs-state collision, R2) ──
326
- const usScoped =
327
- localeCountry === 'US' ||
328
- parsed.regions.some((r) => {
329
- const f = fold(r.name);
330
- return usStateIndex.has(f) && !countryIndex.has(f);
331
- }) ||
332
- parsed.regions.some(
333
- (r) =>
334
- r.scope === 'US' ||
335
- r.scope?.startsWith('US-') ||
336
- usStateFromBareScope(r.scope) !== null
337
- ) ||
338
- parsed.pois.some(
339
- (p) =>
340
- p.pos.kind === 'name' &&
341
- (p.pos.scope?.startsWith('US-') ||
342
- usStateFromBareScope(p.pos.scope) !== null)
343
- );
344
-
345
- // ── Regions (R2/R12) ──
346
- const regions: ResolvedRegion[] = [];
347
- const seenRegion = new Map<string, number>(); // iso → index in regions
348
- let usSubdivisionReferenced = false;
349
- // AK/HI national guard (map-us-subnational-zoom): a choropleth that colors
350
- // Alaska or Hawaii is inherently a national composite map (the insets only make
351
- // sense on albers-usa), so it never sub-national auto-zooms. Tracked ISO-based
352
- // here because `referencedRegionIds` is not in scope at the projection block.
353
- let hasAlaskaRef = false;
354
- let hasHawaiiRef = false;
355
- const referencedRegionIds: { topo: 'us'; id: string }[] = [];
356
- for (const r of parsed.regions) {
357
- const f = fold(r.name);
358
- // Country match: folded name, then ISO-code key (featureIndex id keys), then
359
- // the long-form→NE-abbrev alias table (#6).
360
- const inCountry =
361
- countryIndex.get(f) ??
362
- (REGION_ALIASES[f] ? countryIndex.get(REGION_ALIASES[f]!) : undefined);
363
- const inState = usStateIndex.get(f);
364
- let chosen: {
365
- id: string;
366
- name: string;
367
- layer: 'country' | 'us-state';
368
- } | null = null;
369
- // Explicit ISO scope (§24B.8): force the country-vs-state pick and skip the
370
- // ambiguity warning. `US`/`US-XX` → state; any other 2-letter code → country.
371
- const scope = r.scope;
372
- if (scope) {
373
- const wantsState = scope === 'US' || scope.startsWith('US-');
374
- if (wantsState && inState) {
375
- if (scope.startsWith('US-') && inState.id !== scope) {
376
- diagnostics.push(
377
- emit(MAP_DX.SCOPE_MISS, r.lineNumber, {
378
- name: r.name,
379
- scope,
380
- iso: inState.id,
381
- })
382
- );
383
- continue;
384
- }
385
- chosen = { ...inState, layer: 'us-state' };
386
- } else if (!wantsState && inCountry) {
387
- chosen = { ...inCountry, layer: 'country' };
388
- } else {
389
- diagnostics.push(
390
- emit(MAP_DX.SCOPE_MISS, r.lineNumber, { name: r.name, scope })
391
- );
392
- continue;
393
- }
394
- } else if (inCountry && inState) {
395
- if (usScoped) {
396
- // A US scope (e.g. `locale US`) makes the state the unambiguous
397
- // intent — resolve silently, no disambiguation warning needed.
398
- chosen = { ...inState, layer: 'us-state' };
399
- } else {
400
- chosen = { ...inCountry, layer: 'country' };
401
- // Teach the disambiguation syntax so the author can pin it explicitly.
402
- // Suggest the non-redundant forms: a bare ISO code, or name + scope.
403
- diagnostics.push(
404
- emit(MAP_DX.REGION_AMBIGUOUS, r.lineNumber, {
405
- name: r.name,
406
- layer: chosen.layer,
407
- chosenId: chosen.id,
408
- stateId: inState.id,
409
- countryId: inCountry.id,
410
- })
411
- );
412
- }
413
- } else if (inState) {
414
- chosen = { ...inState, layer: 'us-state' };
415
- } else if (inCountry) {
416
- chosen = { ...inCountry, layer: 'country' };
417
- }
418
- if (!chosen) {
419
- const hint = suggest(r.name, allNames);
420
- diagnostics.push(
421
- emit(MAP_DX.UNKNOWN_SUBDIVISION, r.lineNumber, { name: r.name, hint })
422
- );
423
- continue;
424
- }
425
- if (chosen.layer === 'us-state') {
426
- usSubdivisionReferenced = true;
427
- if (chosen.id === 'US-AK') hasAlaskaRef = true;
428
- if (chosen.id === 'US-HI') hasHawaiiRef = true;
429
- referencedRegionIds.push({ topo: 'us', id: chosen.id });
430
- }
431
- const resolved: ResolvedRegion = {
432
- iso: chosen.id,
433
- name: chosen.name,
434
- layer: chosen.layer,
435
- ...(r.value !== undefined && { value: r.value }),
436
- ...(r.color !== undefined && { color: r.color }),
437
- tags: r.tags,
438
- meta: r.meta,
439
- lineNumber: r.lineNumber,
440
- };
441
- const prev = seenRegion.get(chosen.id);
442
- if (prev !== undefined) {
443
- diagnostics.push(
444
- emit(MAP_DX.DUPLICATE_REGION, r.lineNumber, { name: chosen.name })
445
- );
446
- regions[prev] = resolved;
447
- } else {
448
- seenRegion.set(chosen.id, regions.length);
449
- regions.push(resolved);
450
- }
451
- }
452
-
453
- // ── POIs: two-pass (R9) ──
454
- const registry = new Map<string, Writable<ResolvedPoi>>();
455
- const pois: Writable<ResolvedPoi>[] = [];
456
-
457
- const registerPoi = (
458
- id: string,
459
- poi: Writable<ResolvedPoi>,
460
- line: number
461
- ): void => {
462
- const existing = registry.get(id);
463
- if (existing) {
464
- // An implicit endpoint POI must never clobber an explicitly declared one
465
- // (#8). resolveEndpoint already guards with `registry.has`, but keep the
466
- // invariant local to registration so no caller can violate it.
467
- if (poi.implicit && !existing.implicit) return;
468
- // Only a declared-over-declared collision is a user-facing duplicate.
469
- if (!poi.implicit && !existing.implicit) {
470
- diagnostics.push(emit(MAP_DX.DUPLICATE_POI, line, { id }));
471
- }
472
- const idx = pois.indexOf(existing);
473
- if (idx >= 0) pois[idx] = poi;
474
- } else {
475
- pois.push(poi);
476
- }
477
- registry.set(id, poi);
478
- };
479
-
480
- /** Resolve a name (+optional scope) against the gazetteer (#5 discriminated). */
481
- const lookupName = (
482
- name: string,
483
- scope: string | undefined,
484
- line: number,
485
- scopeHint: string | undefined,
486
- allowAmbiguous: boolean
487
- ): LookupResult => {
488
- const f = fold(name);
489
- let idxs = data.gazetteer.byName[f];
490
- if (!idxs?.length) {
491
- const aliasIdx = data.gazetteer.alt[f];
492
- if (aliasIdx !== undefined) idxs = [aliasIdx];
493
- }
494
- if (!idxs?.length) {
495
- // Airports are the LOWEST-precedence identifier tier (ADR-2): consulted
496
- // only after city `byName` + `alt` miss, so a real city always wins a
497
- // shared token. Exact folded-code match — never defers. Returns just
498
- // {lat,lon,iso} like a city; the POI renders the user's typed token ("JFK")
499
- // as its label, so there is no airport-specific label work.
500
- const airIdx = data.airports?.airportIata?.[f];
501
- if (airIdx !== undefined) {
502
- const a = data.airports!.airports[airIdx];
503
- if (a) return { kind: 'ok', lat: a[0], lon: a[1], iso: a[2] };
504
- }
505
- // A 3-letter token that resolved to neither a city nor a bundled airport is
506
- // almost always an airport code the set doesn't cover — give the targeted
507
- // hint (with the `as XXX` coords escape hatch) instead of the generic miss.
508
- if (/^[A-Za-z]{3}$/.test(name)) {
509
- const code = name.toUpperCase();
510
- diagnostics.push(emit(MAP_DX.UNKNOWN_AIRPORT_CODE, line, { code }));
511
- return { kind: 'miss' };
512
- }
513
- const cityNames = data.gazetteer.cities.map((c) => c[4]);
514
- const hint = suggest(name, cityNames);
515
- diagnostics.push(emit(MAP_DX.UNKNOWN_PLACE, line, { name, hint }));
516
- return { kind: 'miss' };
517
- }
518
- let cands = idxs.map((i) => data.gazetteer.cities[i]!);
519
- const scopeUse = scope ?? scopeHint;
520
- if (scopeUse) {
521
- // ISO 3166-2 subdivision scope is `XX-…` (two letters + dash). A bare
522
- // two-letter token that names a US state resolves as the `US-XX`
523
- // subdivision (`CA` = California, §24B.8) — NOT the colliding country
524
- // code; any other bare two-letter token is a 3166-1 country code.
525
- const bareState = usStateFromBareScope(scopeUse);
526
- const subScope = /^[A-Za-z]{2}-/.test(scopeUse) ? scopeUse : bareState;
527
- const filtered = cands.filter((c) =>
528
- subScope ? c[5] === subScope : c[2] === scopeUse
529
- );
530
- if (filtered.length) cands = filtered;
531
- else if (scope) {
532
- diagnostics.push(
533
- emit(MAP_DX.SCOPE_MISS, line, { name, scope, place: true })
534
- );
535
- return { kind: 'miss' };
536
- }
537
- }
538
- if (cands.length > 1) {
539
- if (!allowAmbiguous && !scopeUse) {
540
- return { kind: 'defer' }; // ambiguous, no scope → pass B
541
- }
542
- // most-populous; tie-break lowest index (R11 — byName is NOT pop-ordered).
543
- cands = [...cands].sort((a, b) => b[3] - a[3]);
544
- if (!scope) diagnostics.push(emit(MAP_DX.AMBIGUOUS_NAME, line, { name }));
545
- }
546
- const c = cands[0]!;
547
- // Shadow hint (ADR-2/F9): the token resolved to a city but is ALSO a bundled
548
- // IATA code (e.g. `Ufa` city vs UFA airport). Non-blocking, once per code, and
549
- // only for the `byName` overlap set (an `alt`-alias hit whose key differs from
550
- // the IATA fold is not flagged — a documented limit).
551
- if (
552
- data.airports?.airportIata?.[f] !== undefined &&
553
- !shadowedAirports.has(f)
554
- ) {
555
- shadowedAirports.add(f);
556
- const code = name.toUpperCase();
557
- diagnostics.push(
558
- emit(MAP_DX.AIRPORT_SHADOWED_BY_CITY, line, { name, code })
559
- );
560
- }
561
- return { kind: 'ok', lat: c[0], lon: c[1], iso: c[2] };
562
- };
563
-
564
- const poiIdFor = (pos: PoiPos, alias: string | undefined): string => {
565
- if (alias) return fold(alias);
566
- if (pos.kind === 'coords') return `@${pos.lat},${pos.lon}`;
567
- return fold(pos.name);
568
- };
569
-
570
- // POI-country tally, fed to default-country inference (#3) and the US-dominant
571
- // projection test (#13). Named POIs contribute their gazetteer ISO; bare-coord
572
- // POIs contribute a rough US-or-not classification (no reverse-geocode).
573
- const poiCountries: string[] = [];
574
- let anyUsPoi = false;
575
- // `anyNonNaPoi` = a POI outside North America (not US/CA/MX). Gates the relaxed
576
- // US-orientation test (#13): US + Canada/Mexico content keeps `albers-usa`, but
577
- // anything beyond NA forces a geographic projection that frames all content.
578
- let anyNonNaPoi = false;
579
- const noteCountry = (iso: string | undefined): void => {
580
- if (iso) {
581
- poiCountries.push(iso);
582
- if (iso === 'US') anyUsPoi = true;
583
- if (iso !== 'US' && iso !== 'CA' && iso !== 'MX') anyNonNaPoi = true;
584
- }
585
- };
586
-
587
- // Pass A: coords + scoped + single-candidate (unambiguous). Defer ambiguous.
588
- const deferred: (typeof parsed.pois)[number][] = [];
589
- for (const p of parsed.pois) {
590
- if (p.pos.kind === 'coords') {
591
- if (looksUS(p.pos.lat, p.pos.lon)) anyUsPoi = true;
592
- else if (!looksNorthAmericaNeighbor(p.pos.lat, p.pos.lon))
593
- anyNonNaPoi = true;
594
- addResolvedPoi(p.pos.lat, p.pos.lon, p);
595
- continue;
596
- }
597
- const got = lookupName(
598
- p.pos.name,
599
- p.pos.scope,
600
- p.lineNumber,
601
- undefined,
602
- false
603
- );
604
- if (got.kind === 'ok') {
605
- noteCountry(got.iso);
606
- addResolvedPoi(got.lat, got.lon, p);
607
- } else if (got.kind === 'defer') {
608
- deferred.push(p); // ambiguous, not an error → pass B
609
- }
610
- // `miss` already errored; drop.
611
- }
612
-
613
- // Infer default-country from explicit directive or the most common ISO across
614
- // resolved regions AND Pass-A POIs (#3 — POIs were previously voided, so a
615
- // POI-only US map never inferred US).
616
- const inferredCountry =
617
- localeCountry ?? mostCommonCountry(regions, poiCountries) ?? undefined;
618
- // A `locale US-GA` subdivision further scopes ambiguous bare cities to that
619
- // state (soft preference — see lookupName); else fall back to the country.
620
- const inferredScope = localeSubdivision ?? inferredCountry;
621
-
622
- // Pass B: ambiguous bare names, scoped by the inferred locale.
623
- for (const p of deferred) {
624
- if (p.pos.kind !== 'name') continue;
625
- const got = lookupName(
626
- p.pos.name,
627
- p.pos.scope,
628
- p.lineNumber,
629
- inferredScope,
630
- true
631
- );
632
- if (got.kind === 'ok') {
633
- noteCountry(got.iso);
634
- addResolvedPoi(got.lat, got.lon, p);
635
- }
636
- }
637
-
638
- function addResolvedPoi(
639
- lat: number,
640
- lon: number,
641
- p: (typeof parsed.pois)[number]
642
- ): void {
643
- const id = poiIdFor(p.pos, p.alias);
644
- const name = p.pos.kind === 'name' ? p.pos.name : p.alias;
645
- const poi: Writable<ResolvedPoi> = {
646
- id,
647
- ...(name !== undefined && { name }),
648
- lat,
649
- lon,
650
- ...(p.label !== undefined && { label: p.label }),
651
- ...(p.color !== undefined && { color: p.color }),
652
- tags: p.tags,
653
- meta: p.meta,
654
- lineNumber: p.lineNumber,
655
- };
656
- registerPoi(id, poi, p.lineNumber);
657
- }
658
-
659
- // ── Edges + routes: bind endpoints, create implicit POIs (R7/R8) ──
660
- // A POI declared with an alias (`poi Chicago as central`) registers under its
661
- // id (`central`), so an endpoint that references it by NAME (`… -> Chicago`)
662
- // misses the id registry. Map declared names → id so such a reference binds to
663
- // the existing POI instead of spawning a duplicate implicit one on the same
664
- // spot (only aliased POIs need this — a name-only POI's id already IS its
665
- // folded name).
666
- const declaredByName = new Map<string, string>();
667
- for (const p of pois) {
668
- const fn = p.name ? fold(p.name) : undefined;
669
- if (fn && fn !== p.id && !declaredByName.has(fn))
670
- declaredByName.set(fn, p.id);
671
- }
672
- const resolveEndpoint = (ref: string, line: number): string | null => {
673
- const f = fold(ref);
674
- if (registry.has(f)) return f;
675
- const aliased = declaredByName.get(f);
676
- if (aliased) return aliased;
677
- const got = lookupName(ref, undefined, line, inferredCountry, true);
678
- if (got.kind !== 'ok') return null;
679
- noteCountry(got.iso);
680
- const poi: Writable<ResolvedPoi> = {
681
- id: f,
682
- name: ref,
683
- lat: got.lat,
684
- lon: got.lon,
685
- tags: {},
686
- meta: {},
687
- lineNumber: line,
688
- implicit: true,
689
- };
690
- registerPoi(f, poi, line);
691
- return f;
692
- };
693
-
694
- const edges: ResolvedEdge[] = [];
695
- for (const e of parsed.edges) {
696
- const fromId = resolveEndpoint(e.from, e.lineNumber);
697
- const toId = resolveEndpoint(e.to, e.lineNumber);
698
- if (!fromId || !toId) continue; // ungeocodable endpoint → drop (error already pushed)
699
- edges.push({
700
- fromId,
701
- toId,
702
- ...(e.label !== undefined && { label: e.label }),
703
- directed: e.directed,
704
- style: e.style,
705
- meta: e.meta,
706
- tags: e.tags,
707
- lineNumber: e.lineNumber,
708
- });
709
- }
710
-
711
- // Resolve a route stop to a POI id, registering it (with its inline tags/label/
712
- // size value) or binding to an already-declared POI. Returns null if a named
713
- // stop can't be geocoded. Unlike the old code, stop metadata is NO LONGER
714
- // dropped — it rides onto the created POI (fixes the named-stop meta bug).
715
- const resolveStop = (
716
- pos: PoiPos,
717
- alias: string | undefined,
718
- label: string | undefined,
719
- tags: Readonly<Record<string, string>>,
720
- sizeValue: string | undefined,
721
- line: number
722
- ): string | null => {
723
- const meta: Record<string, string> =
724
- sizeValue !== undefined ? { value: sizeValue } : {};
725
- if (pos.kind === 'coords') {
726
- const id = alias ? fold(alias) : `@${pos.lat},${pos.lon}`;
727
- if (looksUS(pos.lat, pos.lon)) anyUsPoi = true;
728
- else if (!looksNorthAmericaNeighbor(pos.lat, pos.lon)) anyNonNaPoi = true;
729
- if (!registry.has(id)) {
730
- registerPoi(
731
- id,
732
- {
733
- id,
734
- ...(alias !== undefined && { name: alias }),
735
- lat: pos.lat,
736
- lon: pos.lon,
737
- ...(label !== undefined && { label }),
738
- tags,
739
- meta,
740
- lineNumber: line,
741
- },
742
- line
743
- );
744
- }
745
- return id;
746
- }
747
- // Named stop: bind to an existing declared POI if present, else geocode + create.
748
- const f = fold(pos.name);
749
- if (registry.has(f)) return f;
750
- const aliased = declaredByName.get(f);
751
- if (aliased) return aliased;
752
- const got = lookupName(pos.name, pos.scope, line, inferredScope, true);
753
- if (got.kind !== 'ok') return null;
754
- noteCountry(got.iso);
755
- registerPoi(
756
- f,
757
- {
758
- id: f,
759
- name: pos.name,
760
- lat: got.lat,
761
- lon: got.lon,
762
- ...(label !== undefined && { label }),
763
- tags,
764
- meta,
765
- lineNumber: line,
766
- },
767
- line
768
- );
769
- return f;
770
- };
771
-
772
- const routes: ResolvedRoute[] = [];
773
- for (const rt of parsed.routes) {
774
- const originId = resolveStop(
775
- rt.origin,
776
- rt.originAlias,
777
- rt.originLabel,
778
- rt.originTags,
779
- rt.originValue,
780
- rt.lineNumber
781
- );
782
- if (!originId) continue; // can't anchor the route → drop (error already pushed)
783
- const stopIds: string[] = [originId];
784
- const legs: ResolvedRouteLeg[] = [];
785
- let prevId = originId;
786
- for (const leg of rt.legs) {
787
- const destId = resolveStop(
788
- leg.dest,
789
- leg.destAlias,
790
- leg.destLabel,
791
- {}, // a leg tag colours the LINE (§24B.6), not the destination stop
792
- undefined, // a leg's `width:` is leg thickness, not the dest's size
793
- leg.lineNumber
794
- );
795
- if (!destId) continue; // ungeocodable destination → skip this leg
796
- legs.push({
797
- fromId: prevId,
798
- toId: destId,
799
- ...(leg.label !== undefined && { label: leg.label }),
800
- style: leg.style,
801
- ...(leg.value !== undefined && { value: leg.value }),
802
- tags: leg.tags,
803
- lineNumber: leg.lineNumber,
804
- });
805
- if (!stopIds.includes(destId)) stopIds.push(destId); // unique markers (loop-close dedupe)
806
- prevId = destId;
807
- }
808
- routes.push({ stopIds, legs, lineNumber: rt.lineNumber });
809
- }
810
-
811
- // ── Basemaps + scope ──
812
- // "US-oriented" = there is US content AND all other content stays within North
813
- // America (US, Canada, or Mexico — no non-NA POI, no non-US/CA/MX country
814
- // fill). Such a map renders as the conventional US states map: the full state
815
- // mesh on albers-usa, every state outlined even with no data — including a
816
- // POI-only named-city map (§24B.2), and now also a US map with a Canadian or
817
- // Mexican neighbour POI/fill (the neighbour is framed alongside, never clipped).
818
- // "Has US content" is STATE-level (a US state fill, a US POI, or `locale US`)
819
- // — NOT a country-level `United States` fill, which means "treat the US as one
820
- // unit" and should keep the country shape, not explode into 50 states.
821
- const hasUsContent =
822
- usSubdivisionReferenced || anyUsPoi || localeCountry === 'US';
823
- const usOriented =
824
- !anyNonNaPoi &&
825
- !regions.some(
826
- (r) => r.layer === 'country' && !['US', 'CA', 'MX'].includes(r.iso)
827
- ) &&
828
- hasUsContent;
829
-
830
- const subdivisions: Array<'us-states'> = [];
831
- // Draw the US state mesh in two cases:
832
- // 1. `usSubdivisionReferenced` — a US state is named as a data region
833
- // (e.g. `California heat: 92`), so the states ARE the subject; detail
834
- // them even on a global projection alongside non-NA content.
835
- // 2. `usOriented` — US content with everything else inside North America,
836
- // i.e. the conventional US states map (including a POI-only named-city
837
- // map, or a US map with a Canadian/Mexican neighbour).
838
- // Deliberately NOT drawn for bare US POIs on an otherwise-global map (e.g. a
839
- // worldwide backbone with `us-east-1` + Tokyo + Mumbai): the map already knows
840
- // it spans beyond NA (`anyNonNaPoi`), so exploding the US into 50 states reads
841
- // as noise, not signal. Such a map renders country-only.
842
- if (usSubdivisionReferenced || usOriented) subdivisions.push('us-states');
843
-
844
- // ── Extent + projection (R5/R10) ──
845
- const regionBoxes: GeoExtent[] = [];
846
- for (const ref of referencedRegionIds) {
847
- const bb = featureBbox(data.usStates, ref.id);
848
- if (bb) regionBoxes.push(bb);
849
- }
850
- // country regions contribute their country bbox — but framed on the dominant
851
- // landmass, ignoring far-detached minor territories (e.g. French Guiana) so a
852
- // Europe map naming France doesn't auto-fit across the Atlantic (R5).
853
- for (const r of regions) {
854
- if (r.layer === 'country') {
855
- const bb = featureBboxPrimary(data.worldCoarse, r.iso);
856
- if (bb) regionBoxes.push(bb);
857
- }
858
- }
859
- const points: Array<[number, number]> = pois.map((p) => [p.lon, p.lat]);
860
- const unioned = unionExtent(regionBoxes, points);
861
- // Sub-national US auto-zoom gate (map-us-subnational-zoom). Computed from the
862
- // RAW unpadded union (NOT the padded `extent` below, which inflates area ~1.5×).
863
- // `usSubNational` decides WHETHER to zoom; the projection-family picker below
864
- // still keys off span as before. `localeUsForced` = `locale US` with no
865
- // subdivision → the author asked for the whole-country frame, so stay national
866
- // even on compact data.
867
- const usSubNational = isSubNationalUsExtent(unioned);
868
- const localeUsForced = localeCountry === 'US' && !localeSubdivision;
869
- const DEFAULT_EXTENT: GeoExtent = [
870
- [-180, -85],
871
- [180, 85],
872
- ];
873
- // Region/choropleth maps get a wider context pad; POI maps stay tight (the
874
- // isPoiOnly block below re-pads container bboxes with PAD_FRACTION anyway).
875
- const basePad = regions.length > 0 ? REGION_PAD_FRACTION : PAD_FRACTION;
876
- let extent: GeoExtent = unioned ? pad(unioned, basePad) : DEFAULT_EXTENT; // empty → default
877
-
878
- const isPoiOnly = pois.length > 0 && regions.length === 0;
879
-
880
- // POI-only region framing (R-poi-region): snap the frame to the region(s) that
881
- // CONTAIN the POIs, not an arbitrary window. Reverse-geocode each POI to its US
882
- // state / country (point-in-polygon), then frame to the union of those regions'
883
- // bboxes — so a cluster of Bay Area cities shows the whole of California, with
884
- // the containing region available for labelling. Falls back to the raw POI
885
- // extent when a POI sits outside every polygon (open ocean). The floor below
886
- // still applies as a *minimum* so a tiny container (e.g. Rhode Island) keeps
887
- // breathing room.
888
- const containerRegionIds: string[] = []; // 'US-CA' (state) or 'FR' (country)
889
- if (isPoiOnly) {
890
- const countries = decodeFeatures(data.worldDetail);
891
- const states = decodeFeatures(data.usStates);
892
- const seen = new Set<string>();
893
- const containerBoxes: GeoExtent[] = [];
894
- for (const p of pois) {
895
- const { country, state } = regionAt([p.lon, p.lat], countries, states);
896
- const id = state?.iso ?? country?.iso;
897
- if (!id || seen.has(id)) continue;
898
- seen.add(id);
899
- containerRegionIds.push(id);
900
- // On a US-oriented map a foreign-NEIGHBOUR POI (a Canadian/Mexican city)
901
- // frames by just its POINT, never its whole country — §24B.2: "A Canada/
902
- // Mexico POI expands the frame by just its point (the US barely shrinks)".
903
- // Pulling in Canada's full bbox here would drag the frame up to ~83°N (the
904
- // point is already in `points`, so it still shows). US states still
905
- // contribute their bbox (so a Bay Area cluster frames all of California).
906
- const bb = state
907
- ? featureBbox(data.usStates, id)
908
- : usOriented
909
- ? null
910
- : featureBboxPrimary(data.worldCoarse, id);
911
- // Skip a degenerate whole-sphere bbox: spherical geoBounds returns the full
912
- // globe for a malformed/missing geometry, which would blow the frame out to
913
- // the entire world. Real region geometry never spans the full sphere — fall
914
- // back to the raw POI extent (+ floor) instead.
915
- if (bb && !isWholeSphere(bb)) containerBoxes.push(bb);
916
- }
917
- const containerUnion = unionExtent(containerBoxes, points);
918
- if (containerUnion)
919
- extent = pad(
920
- clampContainerToCluster(containerUnion, points, usOriented),
921
- PAD_FRACTION
922
- );
923
- }
924
-
925
- // POI-only fit-to-cluster zoom floor. With region framing above, the extent is
926
- // usually the containing region(s); the floor only kicks in for a tiny region
927
- // or a POI that fell outside every polygon. Expand a too-tight extent
928
- // symmetrically about its centroid until the longer axis reaches
929
- // POI_ZOOM_FLOOR_DEG so recognizable land always frames the dots. Uniform scale
930
- // preserves the aspect; the layout's fitExtent letterboxes to canvas.
931
- // Also applies to a sub-national US region map (map-us-subnational-zoom) so a
932
- // single tiny state (e.g. Rhode Island) keeps neighbour context instead of
933
- // over-zooming into near-blank land. `usSubNational` is derived from the raw
934
- // union above, so flooring the framing `extent` here doesn't feed back into it.
935
- // Gated on `usOriented` — `usSubNational` is a pure area-vs-CONUS ratio, true
936
- // for ANY small bbox, so without this guard a compact non-US region (e.g. a
937
- // single small European country) would be floored too, regressing AC10.
938
- if (isPoiOnly || (usSubNational && usOriented)) {
939
- const cx = (extent[0][0] + extent[1][0]) / 2;
940
- const cy = (extent[0][1] + extent[1][1]) / 2;
941
- const lon = extent[1][0] - extent[0][0];
942
- const lat = extent[1][1] - extent[0][1];
943
- const longer = Math.max(lon, lat);
944
- if (longer > 0 && longer < POI_ZOOM_FLOOR_DEG) {
945
- const k = POI_ZOOM_FLOOR_DEG / longer;
946
- const halfLon = (lon * k) / 2;
947
- const halfLat = (lat * k) / 2;
948
- extent = [
949
- [cx - halfLon, cy - halfLat],
950
- [cx + halfLon, cy + halfLat],
951
- ];
952
- }
953
- }
954
-
955
- const lonSpan = extent[1][0] - extent[0][0];
956
- const latSpan = extent[1][1] - extent[0][1];
957
- const span = Math.max(lonSpan, latSpan);
958
- const maxAbsLat = Math.max(Math.abs(extent[0][1]), Math.abs(extent[1][1]));
959
- // albers-usa is the national US composite (insets AK/HI when referenced, the
960
- // conic projects neighbour land around the states). Choose it exactly when the
961
- // map is US-oriented (#13): US content plus only US/Canada/Mexico elsewhere.
962
- // Content reaching BEYOND North America fails `usOriented` and stays on a
963
- // geographic world/regional projection that frames everything. Note: this
964
- // intentionally snaps a POI-only US city map to the national frame ("show all
965
- // states") rather than fit-zooming to the cluster on a geographic projection.
966
- // (§24B.2 — projection is inferred, never configured.)
967
- // A compact US region/choropleth that should zoom rather than frame the nation
968
- // (map-us-subnational-zoom): US-oriented, area-compact, not a POI map, and not
969
- // forced national by an AK/HI inset or `locale US`.
970
- const usSubNationalZoom =
971
- usOriented &&
972
- usSubNational &&
973
- !isPoiOnly &&
974
- !hasAlaskaRef &&
975
- !hasHawaiiRef &&
976
- !localeUsForced;
977
- let projection: ProjectionFamily;
978
- if (
979
- usOriented &&
980
- lonSpan < US_NATIONAL_LON_SPAN &&
981
- (isPoiOnly || usSubNationalZoom)
982
- ) {
983
- // Sub-national US, zoomed: regional Mercator. North is straight up everywhere
984
- // (vertical meridians) — a conic equal-area fans its meridians toward the pole
985
- // so an off-centre region visibly tilts, which reads wrong for a "zoom into the
986
- // US" view. Area distortion across a sub-national frame is negligible, and the
987
- // POI-cluster path already uses Mercator here. Covers both a POI cluster (fit
988
- // to the floored extent) and a compact region/choropleth. The us-states mesh
989
- // is still drawn (subdivisions via usOriented), so neighbours frame the data.
990
- projection = 'mercator';
991
- } else if (usOriented) {
992
- // National US frame: spread-out region data, a wide corridor (lonSpan ≥ the
993
- // national threshold), a POI map that spans the country, an AK/HI composite,
994
- // or an explicit `locale US` whole-country request. (§24B.2 — albers-usa.)
995
- projection = 'albers-usa';
996
- } else if (span > WORLD_SPAN || maxAbsLat > MERCATOR_MAX_LAT) {
997
- // World/multi-continent scale (or a polar-reaching frame). Every world map —
998
- // data choropleth OR dataless reference — gets equirectangular: a clean,
999
- // conventional rectangular wall-map frame. (Trade: not equal-area, so a
1000
- // choropleth's high-latitude regions stretch vertically — accepted for the
1001
- // consistent rectangular look over Equal Earth's area honesty.)
1002
- projection = 'equirectangular';
1003
- } else {
1004
- // Tight clusters AND single-continent regional views: a conic equal-area
1005
- // (Albers) projection, parameterized per-extent in the layout. This is the
1006
- // conventional atlas projection for a single mid-latitude continent — it
1007
- // keeps familiar country shapes (no world-projection horizontal squash) and,
1008
- // unlike Mercator, does NOT vertically inflate high-latitude land. That
1009
- // inflation is what pushed a Europe choropleth's colored mass into the bottom
1010
- // ~30% of the frame (Scandinavia's stretch wasted the upper half on empty
1011
- // ocean); equal-area framing re-centers the data and scales the land larger.
1012
- projection = 'conic-equal-area';
1013
- }
1014
-
1015
- // World-scale framing (R10): a multi-continent spread frames most cleanly as
1016
- // the conventional Greenwich-centred world rectangle. The tight-arc longitude
1017
- // union is unstable for sparse global points — and an antimeridian-crossing
1018
- // country box (the US, via its Aleutians) wraps the union to an Asia-centred
1019
- // window that splits the Americas at the seam. When the data occupies at
1020
- // least half the globe in longitude, snap to full longitude AND widen the
1021
- // latitude band to the populated world (≈ Tierra del Fuego → northern
1022
- // Russia/Canada) so the map reads as a standard world view with every
1023
- // continent shown — not a thin band cropped to the data's latitudes (which
1024
- // would slice off South Africa, southern Argentina, northern Russia, …). The
1025
- // ≥180° gate leaves regional spreads tight — `region` continents (Europe
1026
- // ≈70°, Asia ≈155°) and antimeridian clusters (mercator anyway) untouched.
1027
- // Applies to the equirectangular world projection (data + reference alike).
1028
- if (lonSpan >= 180) {
1029
- extent = [
1030
- [-180, Math.min(extent[0][1], WORLD_LAT_SOUTH)],
1031
- [180, Math.max(extent[1][1], WORLD_LAT_NORTH)],
1032
- ];
1033
- }
1034
-
1035
- result.regions = regions;
1036
- result.pois = pois;
1037
- result.edges = edges;
1038
- result.routes = routes;
1039
- result.basemaps = {
1040
- // Tier is intentionally pinned to detail (50m) at ALL scales. Diagrammo maps
1041
- // are presentational (palette tints, relief hachures, POI hubs), not
1042
- // survey-grade — recognizability > generalization: 110m coarse drops the
1043
- // Italian boot to a stump at world scale. `WORLD_SPAN` lives on only for the
1044
- // projection decision (the `usOriented`/`span > WORLD_SPAN` chain above); it
1045
- // no longer gates basemap resolution.
1046
- // `worldCoarse` is still loaded — it's the authoritative name/bbox index
1047
- // (featureIndex, featureBboxPrimary), not dead code.
1048
- world: 'detail',
1049
- subdivisions,
1050
- };
1051
- result.extent = extent;
1052
- result.projection = projection;
1053
- result.poiFrameContainers = containerRegionIds;
1054
- result.error = parsed.error ?? firstError(diagnostics);
1055
- // `Writable` widens the GeoExtent tuple to an array; the runtime value is a
1056
- // correct GeoExtent, so cast back on return (through unknown — tuple vs array).
1057
- return result as unknown as ResolvedMap;
1058
- }
1059
-
1060
- function mostCommonCountry(
1061
- regions: ResolvedRegion[],
1062
- poiCountries: string[]
1063
- ): string | undefined {
1064
- const counts = new Map<string, number>();
1065
- for (const r of regions) {
1066
- const iso = r.layer === 'us-state' ? 'US' : r.iso;
1067
- counts.set(iso, (counts.get(iso) ?? 0) + 1);
1068
- }
1069
- // Pass-A-resolved POI countries now count too (#3): a POI-only US map infers
1070
- // `default-country=US`.
1071
- for (const iso of poiCountries) {
1072
- counts.set(iso, (counts.get(iso) ?? 0) + 1);
1073
- }
1074
- // Iterate alphabetically and keep on strictly-greater count, so a tie resolves
1075
- // to the alphabetically-first ISO — deterministic (#14).
1076
- let best: string | undefined;
1077
- let bestN = 0;
1078
- for (const [iso, n] of [...counts.entries()].sort((a, b) =>
1079
- a[0] < b[0] ? -1 : 1
1080
- )) {
1081
- if (n > bestN) {
1082
- bestN = n;
1083
- best = iso;
1084
- }
1085
- }
1086
- return best;
1087
- }
1088
-
1089
- /** Asymmetric container clamp (R-poi-region overshoot guard). Container framing
1090
- * reveals the region(s) holding the POIs, but one POI at the edge of a tall/wide
1091
- * country drags the frame to that country's far edge. Cap how far the frame
1092
- * extends BEYOND the POI cluster on each side at CONTAINER_OVERSHOOT_DEG, while
1093
- * letting a genuinely tighter container edge still bound the frame (so a small
1094
- * country shows whole, but a cluster inside a giant one stays on the cluster).
1095
- *
1096
- * Each longitude side clamps independently. A container edge is a usable outer
1097
- * bound only when it sits within the normal [-180, 180] range AND on the correct
1098
- * side of the cluster; an antimeridian-crossing container (Russia, Fiji, NZ, the
1099
- * US via the Aleutians) reports a degenerate east (> 180, or numerically < its
1100
- * own west), so that side falls back to cluster ± overshoot instead of skipping
1101
- * the clamp entirely (which previously blew a western-Russia cluster out to a
1102
- * world frame). Latitude never wraps, so it always clamps. Assumes the POI
1103
- * cluster itself does not straddle the seam — true for any regional cluster. */
1104
- function clampContainerToCluster(
1105
- container: GeoExtent,
1106
- points: Array<[number, number]>,
1107
- usOriented: boolean
1108
- ): GeoExtent {
1109
- const poi = unionExtent([], points);
1110
- if (!poi) return container;
1111
- let [[west, south], [east, north]] = container;
1112
- const [[pWest, pSouth], [pEast, pNorth]] = poi;
1113
- // Overshoot shrinks with cluster size for non-US maps (see containerOvershoot):
1114
- // a big cluster already orients itself, so it gets less surrounding slack than a
1115
- // tiny one. US-oriented maps keep the flat MAX (the albers-usa gate needs it).
1116
- const over = containerOvershoot(
1117
- Math.max(pEast - pWest, pNorth - pSouth),
1118
- usOriented
1119
- );
1120
- south = Math.max(south, pSouth - over);
1121
- north = Math.min(north, pNorth + over);
1122
- const wOver = pWest - over;
1123
- const eOver = pEast + over;
1124
- // West edge usable iff in range and not east of the cluster's west.
1125
- west = west >= -180 && west <= pWest ? Math.max(west, wOver) : wOver;
1126
- // East edge usable iff in range and not west of the cluster's east.
1127
- east = east <= 180 && east >= pEast ? Math.min(east, eOver) : eOver;
1128
- return [
1129
- [west, south],
1130
- [east, north],
1131
- ];
1132
- }
1133
-
1134
- function pad(e: GeoExtent, frac: number): GeoExtent {
1135
- const dLon = (e[1][0] - e[0][0]) * frac || 1;
1136
- const dLat = (e[1][1] - e[0][1]) * frac || 1;
1137
- return [
1138
- [Math.max(-180, e[0][0] - dLon), Math.max(-90, e[0][1] - dLat)],
1139
- [Math.min(540, e[1][0] + dLon), Math.min(90, e[1][1] + dLat)],
1140
- ] as GeoExtent;
1141
- }
1142
-
1143
- function firstError(diags: DgmoError[]): string | null {
1144
- const e = diags.find((d) => d.severity === 'error');
1145
- return e ? formatDgmoError(e) : null;
1146
- }