riverbankcms 0.23.0 → 0.24.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.
@@ -1478,7 +1478,7 @@ function toPositiveTicketQuantity(value) {
1478
1478
  * null when the total is not a positive whole number.
1479
1479
  */
1480
1480
  function deriveTotalTicketQuantity(lines) {
1481
- const total = lines.reduce((sum, line) => sum + line.quantity, 0);
1481
+ const total = lines.reduce((sum, line$1) => sum + line$1.quantity, 0);
1482
1482
  const parsed = toPositiveTicketQuantity(total);
1483
1483
  return parsed.ok ? parsed.value : null;
1484
1484
  }
@@ -2001,8 +2001,8 @@ function resolveBookingTimezone(venueTimezone, siteDefaultTimezone) {
2001
2001
  return venueTimezone ?? siteDefaultTimezone;
2002
2002
  }
2003
2003
  function resolveBookingTimeZoneId(...candidates) {
2004
- for (const candidate of candidates) {
2005
- const normalized = typeof candidate === "string" ? candidate.trim() : "";
2004
+ for (const candidate$1 of candidates) {
2005
+ const normalized = typeof candidate$1 === "string" ? candidate$1.trim() : "";
2006
2006
  if (normalized && isValidBookingTimeZone(normalized)) {
2007
2007
  return normalized;
2008
2008
  }
@@ -2222,8 +2222,8 @@ function parseBookingLocalDate(value) {
2222
2222
  const year = Number(match.groups.year);
2223
2223
  const month = Number(match.groups.month);
2224
2224
  const day = Number(match.groups.day);
2225
- const candidate = new Date(Date.UTC(year, month - 1, day, 12, 0, 0));
2226
- if (Number.isNaN(candidate.getTime()) || candidate.getUTCFullYear() !== year || candidate.getUTCMonth() + 1 !== month || candidate.getUTCDate() !== day) {
2225
+ const candidate$1 = new Date(Date.UTC(year, month - 1, day, 12, 0, 0));
2226
+ if (Number.isNaN(candidate$1.getTime()) || candidate$1.getUTCFullYear() !== year || candidate$1.getUTCMonth() + 1 !== month || candidate$1.getUTCDate() !== day) {
2227
2227
  throw new Error(`Invalid booking date format: "${value}" (expected YYYY-MM-DD)`);
2228
2228
  }
2229
2229
  return {
@@ -2567,12 +2567,12 @@ function decideRangeLabels(span, context, profile) {
2567
2567
  label: start
2568
2568
  };
2569
2569
  }
2570
- function presentRangeEndpoint(value, context, profile, label) {
2570
+ function presentRangeEndpoint(value, context, profile, label$1) {
2571
2571
  const date = presentDate({
2572
2572
  kind: "instant",
2573
2573
  value
2574
2574
  }, context, profile);
2575
- const time = label === undefined ? presentTime(value, context, profile) : `${presentClock(value, context, profile.clock)} ${label}`;
2575
+ const time = label$1 === undefined ? presentTime(value, context, profile) : `${presentClock(value, context, profile.clock)} ${label$1}`;
2576
2576
  return `${date}, ${time}`;
2577
2577
  }
2578
2578
  function presentNativeRange(span, context, format) {
@@ -3024,7 +3024,7 @@ function selectCategoryView(categories, services, filterCategoryId) {
3024
3024
  layout: publicLayout()
3025
3025
  };
3026
3026
  }
3027
- const category = categories.find((candidate) => candidate.id === filterCategoryId);
3027
+ const category = categories.find((candidate$1) => candidate$1.id === filterCategoryId);
3028
3028
  if (category === undefined) {
3029
3029
  return {
3030
3030
  kind: "fallback-all",
@@ -3113,6 +3113,169 @@ const RECTIFICATION_HISTORY_PROPAGATION_PLANS = {
3113
3113
  blocked_immutable_history: { kind: "blocked_immutable_history" }
3114
3114
  };
3115
3115
 
3116
+ //#endregion
3117
+ //#region ../core/src/map-drawing/model.ts
3118
+ const MAX_DRAWING_BYTES = 1024 * 1024;
3119
+ const DRAWING_SIZE = 800;
3120
+ const DRAWING_SCHEMA_VERSION = 2;
3121
+ const DRAWING_GENERATOR_VERSION = 3;
3122
+ /** Current real city extracts fit with >2x headroom; reservations precede allocation. */
3123
+ const DRAWING_DECODE_LIMITS = {
3124
+ pathsPerFeature: 2048,
3125
+ verticesPerPath: 8192,
3126
+ verticesPerFeature: 32768,
3127
+ featuresPerLayer: 1e4,
3128
+ featuresPerTile: 16e3,
3129
+ featuresPerAttempt: 6e4,
3130
+ verticesPerAttempt: 1e6
3131
+ };
3132
+ /**
3133
+ * The two fixed framings. Neighbourhood draws 1 m per view-box unit, wider 4 m, so the same stroke and label
3134
+ * sizes read well in both. The wider view leaves out buildings, distant paths and small green patches, which turn to
3135
+ * noise at that scale. `minParkArea` is in square view-box units. Labels go to the nearest streets first, with a
3136
+ * minor street counted as `minorLabelDistance` times further away than it is, so the wider view names main roads.
3137
+ */
3138
+ const FRAMINGS = [{
3139
+ name: "neighbourhood",
3140
+ radiusMeters: 400,
3141
+ zoom: 15,
3142
+ roads: [
3143
+ "major",
3144
+ "minor",
3145
+ "path"
3146
+ ],
3147
+ maxPathDistance: 800,
3148
+ buildings: true,
3149
+ minParkArea: .3,
3150
+ maxLabels: 6,
3151
+ minorLabelDistance: 1.5
3152
+ }, {
3153
+ name: "wider",
3154
+ radiusMeters: 1600,
3155
+ zoom: 14,
3156
+ roads: [
3157
+ "major",
3158
+ "minor",
3159
+ "path"
3160
+ ],
3161
+ maxPathDistance: 100,
3162
+ buildings: false,
3163
+ minParkArea: 40,
3164
+ maxLabels: 5,
3165
+ minorLabelDistance: 4
3166
+ }];
3167
+ const tilePoint = z.tuple([z.number().finite().min(-8192).max(12288), z.number().finite().min(-8192).max(12288)]).readonly();
3168
+ const paths = z.array(z.array(tilePoint).min(2).max(DRAWING_DECODE_LIMITS.verticesPerPath).readonly()).min(1).max(DRAWING_DECODE_LIMITS.pathsPerFeature).readonly();
3169
+ const featureFields = {
3170
+ kind: z.string().max(80),
3171
+ name: z.string().trim().max(60).regex(/^[^<>\u0000-\u001f\u007f]*$/).nullish().transform((value) => value === undefined || value === null || value === "" ? null : value),
3172
+ sortRank: z.number().finite().min(-1e4).max(1e4).default(0),
3173
+ paths: paths.refine((value) => value.reduce((n, path) => n + path.length, 0) <= DRAWING_DECODE_LIMITS.verticesPerFeature)
3174
+ };
3175
+ /** An unusable optional label must not discard otherwise valid road geometry. */
3176
+ function normalizeMapDrawingName(value) {
3177
+ const parsed = featureFields.name.safeParse(value);
3178
+ return parsed.success ? parsed.data : null;
3179
+ }
3180
+ const feature = z.discriminatedUnion("geometry", [z.strictObject({
3181
+ ...featureFields,
3182
+ layer: z.literal("roads"),
3183
+ geometry: z.literal("line")
3184
+ }).readonly(), z.strictObject({
3185
+ ...featureFields,
3186
+ layer: z.enum([
3187
+ "earth",
3188
+ "water",
3189
+ "landuse",
3190
+ "buildings"
3191
+ ]),
3192
+ geometry: z.literal("polygon")
3193
+ }).refine((value) => value.paths.every((ring) => ring.length >= 3)).readonly()]);
3194
+ const tile = z.strictObject({
3195
+ z: z.union([z.literal(14), z.literal(15)]),
3196
+ x: z.number().int().nonnegative(),
3197
+ y: z.number().int().nonnegative(),
3198
+ extent: z.literal(4096),
3199
+ features: z.array(feature).max(DRAWING_DECODE_LIMITS.featuresPerTile).readonly()
3200
+ }).refine((value) => value.x < 2 ** value.z && value.y < 2 ** value.z).readonly();
3201
+ const mapDrawingInputSchema = z.strictObject({
3202
+ buildId: z.string().regex(/^\d{8}$/),
3203
+ tileSchemaVersion: z.string().regex(/^4\.\d+\.\d+$/),
3204
+ pin: z.lazy(() => locationPinSchema),
3205
+ tiles: z.array(tile).min(1).max(96).readonly()
3206
+ }).superRefine((value, ctx) => {
3207
+ const features = value.tiles.flatMap((item) => item.features);
3208
+ if (features.length > DRAWING_DECODE_LIMITS.featuresPerAttempt || features.reduce((sum, item) => sum + item.paths.reduce((count, path) => count + path.length, 0), 0) > DRAWING_DECODE_LIMITS.verticesPerAttempt) {
3209
+ ctx.addIssue({
3210
+ code: "custom",
3211
+ message: "Decoded geometry exceeds the attempt budget."
3212
+ });
3213
+ }
3214
+ }).readonly();
3215
+ const point = z.tuple([z.number().min(0).max(DRAWING_SIZE), z.number().min(0).max(DRAWING_SIZE)]).readonly();
3216
+ const polygon = z.strictObject({
3217
+ kind: z.literal("polygon"),
3218
+ fillRule: z.literal("evenodd"),
3219
+ className: z.enum([
3220
+ "map-land",
3221
+ "map-water",
3222
+ "map-park",
3223
+ "map-building"
3224
+ ]),
3225
+ rings: z.array(z.array(point).min(3).max(8192).readonly()).min(1).max(DRAWING_DECODE_LIMITS.pathsPerFeature).readonly()
3226
+ }).readonly();
3227
+ const line = z.strictObject({
3228
+ kind: z.literal("line"),
3229
+ className: z.enum([
3230
+ "map-road-major-casing",
3231
+ "map-road-major",
3232
+ "map-road-minor-casing",
3233
+ "map-road-minor",
3234
+ "map-road-path"
3235
+ ]),
3236
+ points: z.array(point).min(2).max(8192).readonly()
3237
+ }).readonly();
3238
+ const drawingPathSchema = z.discriminatedUnion("kind", [polygon, line]);
3239
+ const label = z.strictObject({
3240
+ className: z.literal("map-label"),
3241
+ haloClassName: z.literal("map-label-halo"),
3242
+ text: z.string().trim().min(1).max(60).regex(/^[^<>\u0000-\u001f\u007f]*$/),
3243
+ point,
3244
+ angle: z.number().min(-90).max(90)
3245
+ }).readonly();
3246
+ const drawingFrameSchema = z.strictObject({
3247
+ radiusMeters: z.union([z.literal(FRAMINGS[0].radiusMeters), z.literal(FRAMINGS[1].radiusMeters)]),
3248
+ viewBox: z.tuple([
3249
+ z.literal(0),
3250
+ z.literal(0),
3251
+ z.literal(800),
3252
+ z.literal(800)
3253
+ ]).readonly(),
3254
+ backgroundClass: z.literal("map-water"),
3255
+ paths: z.array(drawingPathSchema).max(1800).readonly(),
3256
+ labels: z.array(label).max(12).readonly(),
3257
+ pin: z.strictObject({
3258
+ className: z.literal("map-pin"),
3259
+ point: z.tuple([z.literal(400), z.literal(400)]).readonly()
3260
+ }).readonly()
3261
+ }).superRefine((value, ctx) => {
3262
+ const count = value.paths.reduce((sum, path) => sum + (path.kind === "line" ? path.points.length : path.rings.reduce((n, ring) => n + ring.length, 0)), 0);
3263
+ if (count > 5e4) ctx.addIssue({
3264
+ code: "custom",
3265
+ message: "Drawing exceeds its vertex budget."
3266
+ });
3267
+ }).readonly();
3268
+ const mapDrawingSchema = z.strictObject({
3269
+ schemaVersion: z.literal(DRAWING_SCHEMA_VERSION),
3270
+ generatorVersion: z.literal(DRAWING_GENERATOR_VERSION),
3271
+ buildId: z.string().regex(/^\d{8}$/),
3272
+ tileSchemaVersion: z.string().regex(/^4\.\d+\.\d+$/),
3273
+ frames: z.strictObject({
3274
+ neighbourhood: drawingFrameSchema,
3275
+ wider: drawingFrameSchema
3276
+ }).readonly()
3277
+ }).refine((value) => value.frames.neighbourhood.radiusMeters === FRAMINGS[0].radiusMeters && value.frames.wider.radiusMeters === FRAMINGS[1].radiusMeters).refine((value) => new TextEncoder().encode(JSON.stringify(value)).byteLength <= MAX_DRAWING_BYTES, { error: "Drawing exceeds its byte budget." }).readonly();
3278
+
3116
3279
  //#endregion
3117
3280
  //#region ../core/src/string.ts
3118
3281
  /**
@@ -3200,6 +3363,7 @@ const locationValueSchema = z.strictObject({
3200
3363
  pin: locationPinSchema.nullable().default(null),
3201
3364
  doorNote: z.string().trim().max(1e3).nullish().transform(trimmedStringOrNull),
3202
3365
  addressMatch: locationAddressMatchSchema.nullable().default(null),
3366
+ addressCheckSettled: z.literal(true).optional(),
3203
3367
  ownerConfirmedPinAndAddress: z.boolean().default(false)
3204
3368
  }).superRefine((value, context) => {
3205
3369
  if (value.address === null && value.pin === null) {
@@ -3216,6 +3380,13 @@ const locationValueSchema = z.strictObject({
3216
3380
  message: "Address evidence requires an address."
3217
3381
  });
3218
3382
  }
3383
+ if (value.address === null && value.addressCheckSettled) {
3384
+ context.addIssue({
3385
+ code: "custom",
3386
+ path: ["addressCheckSettled"],
3387
+ message: "A settled address check requires an address."
3388
+ });
3389
+ }
3219
3390
  if (value.ownerConfirmedPinAndAddress && (value.address === null || value.pin === null)) {
3220
3391
  context.addIssue({
3221
3392
  code: "custom",
@@ -3232,7 +3403,8 @@ function parseLocationValue(input) {
3232
3403
  issues: parsed.error.issues
3233
3404
  });
3234
3405
  }
3235
- function formatLocationAddress(address) {
3406
+ /** One display formatter: a known matching site country is redundant. */
3407
+ function formatLocationAddress(address, siteCountry) {
3236
3408
  if (address === null) return null;
3237
3409
  const parts = [
3238
3410
  address.building,
@@ -3242,15 +3414,32 @@ function formatLocationAddress(address) {
3242
3414
  address.locality,
3243
3415
  address.region,
3244
3416
  address.postalCode,
3245
- address.country
3417
+ sameLocationCountry(address.country, siteCountry) ? null : address.country
3246
3418
  ];
3247
3419
  return parts.filter((part) => part !== null).join(", ") || null;
3248
3420
  }
3421
+ const regionNames = new Intl.DisplayNames(["en"], { type: "region" });
3422
+ const countryIdentity = (country) => {
3423
+ const text = country.trim();
3424
+ const code = text.toUpperCase() === "UK" ? "GB" : text.toUpperCase();
3425
+ return (code.length === 2 && /^[A-Z]{2}$/.test(code) ? regionNames.of(code) ?? text : text).toLowerCase();
3426
+ };
3427
+ function sameLocationCountry(country, siteCountry) {
3428
+ return country !== null && Boolean(siteCountry?.trim()) && countryIdentity(country) === countryIdentity(siteCountry ?? "");
3429
+ }
3430
+ const publicLocationSchema = z.strictObject({
3431
+ drawing: z.lazy(() => mapDrawingSchema).optional().catch(undefined),
3432
+ address: locationAddressSchema.nullable(),
3433
+ displayAddress: z.string().nullable().default(null),
3434
+ pin: locationPinSchema.nullable(),
3435
+ doorNote: z.string().max(1e3).nullable(),
3436
+ attribution: z.array(locationAddressMatchSchema.unwrap().shape.attribution.unwrap().element).max(4).readonly()
3437
+ }).readonly();
3249
3438
  /** Permission belongs to the resource/source owner before this projection. */
3250
- function projectPublicLocation(location) {
3439
+ function projectPublicLocation(location, siteCountry) {
3251
3440
  return {
3252
3441
  address: location.address,
3253
- displayAddress: formatLocationAddress(location.address),
3442
+ displayAddress: formatLocationAddress(location.address, siteCountry),
3254
3443
  pin: location.pin,
3255
3444
  doorNote: location.doorNote,
3256
3445
  attribution: location.addressMatch?.attribution ?? []
@@ -3274,6 +3463,73 @@ function toLocationCoordinates(value) {
3274
3463
  lng: value.lng
3275
3464
  };
3276
3465
  }
3466
+ /** Decide permission before loading or projecting the selected source. */
3467
+ function decideLocationSourcePermission(source, businessAddressVisibility) {
3468
+ if (source === "business_address" && businessAddressVisibility === undefined) return { kind: "unresolved" };
3469
+ return source === "business_address" && businessAddressVisibility === "hidden" ? { kind: "hidden" } : {
3470
+ kind: "permitted",
3471
+ source
3472
+ };
3473
+ }
3474
+ function resolvePermittedLocation(permission, location, siteCountry) {
3475
+ if (permission.kind !== "permitted") return permission;
3476
+ return location === null ? {
3477
+ kind: "missing",
3478
+ source: permission.source
3479
+ } : {
3480
+ kind: "visible",
3481
+ source: permission.source,
3482
+ location: projectPublicLocation(location, siteCountry)
3483
+ };
3484
+ }
3485
+
3486
+ //#endregion
3487
+ //#region ../core/src/locationLinks.ts
3488
+ /** Both outbound actions use the arrival pin, never match evidence. */
3489
+ function locationMapLinks(input) {
3490
+ const target = input.location != null ? `${input.location.lat},${input.location.lng}` : normalizeLocationAddressText(input.addressText);
3491
+ if (target === null) return {
3492
+ directions: null,
3493
+ googleMaps: null
3494
+ };
3495
+ const directions = new URL("https://www.google.com/maps/dir/");
3496
+ directions.searchParams.set("api", "1");
3497
+ directions.searchParams.set("destination", target);
3498
+ const search = new URL("https://www.google.com/maps/search/");
3499
+ search.searchParams.set("api", "1");
3500
+ search.searchParams.set("query", target);
3501
+ return {
3502
+ directions: directions.toString(),
3503
+ googleMaps: search.toString()
3504
+ };
3505
+ }
3506
+
3507
+ //#endregion
3508
+ //#region ../core/src/stable-json.ts
3509
+ function stableJsonStringify(value) {
3510
+ return JSON.stringify(sortStableJsonValue(value));
3511
+ }
3512
+ function sortStableJsonValue(value) {
3513
+ if (value === null) return null;
3514
+ if (typeof value === "string" || typeof value === "boolean") return value;
3515
+ if (typeof value === "number") {
3516
+ if (!Number.isFinite(value)) {
3517
+ throw new TypeError("Stable JSON only supports finite numbers");
3518
+ }
3519
+ return value;
3520
+ }
3521
+ if (Array.isArray(value)) {
3522
+ return value.map(sortStableJsonValue);
3523
+ }
3524
+ if (typeof value === "object") {
3525
+ const prototype = Object.getPrototypeOf(value);
3526
+ if (prototype !== Object.prototype && prototype !== null) {
3527
+ throw new TypeError("Stable JSON only supports plain objects");
3528
+ }
3529
+ return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, child]) => [key, sortStableJsonValue(child)]));
3530
+ }
3531
+ throw new TypeError(`Stable JSON does not support ${typeof value}`);
3532
+ }
3277
3533
 
3278
3534
  //#endregion
3279
3535
  //#region ../core/src/location-relationship.ts
@@ -3293,15 +3549,15 @@ function decideLocationRelationship(location) {
3293
3549
  if (location === null) return { kind: "empty" };
3294
3550
  if (location.pin === null) return { kind: "address_only" };
3295
3551
  if (location.address === null) return { kind: "pin_only" };
3296
- if (location.addressMatch === null || location.addressMatch.precision === "unknown") {
3552
+ if (location.addressMatch === null) {
3297
3553
  return { kind: "unchecked" };
3298
3554
  }
3299
- const { point, precision } = location.addressMatch;
3300
- const distanceMetres = locationDistanceMetres(point, location.pin);
3301
- const thresholdMetres = LOCATION_MATCH_THRESHOLDS_METRES[precision];
3555
+ const { point: point$1, precision } = location.addressMatch;
3556
+ const distanceMetres = locationDistanceMetres(point$1, location.pin);
3557
+ const thresholdMetres = LOCATION_MATCH_THRESHOLDS_METRES[precision === "unknown" ? "building" : precision];
3302
3558
  const beyondThreshold = distanceMetres > thresholdMetres * (1 + 8 * Number.EPSILON);
3303
3559
  return {
3304
- kind: beyondThreshold ? "far" : precision === "building" ? "near" : "approximate",
3560
+ kind: beyondThreshold ? "far" : precision === "building" || precision === "unknown" ? "near" : "approximate",
3305
3561
  distanceMetres,
3306
3562
  thresholdMetres,
3307
3563
  precision
@@ -3315,6 +3571,7 @@ const spatialAddressParts = [
3315
3571
  "country"
3316
3572
  ];
3317
3573
  const comparisonText = (text) => (text ?? "").trim().replace(/\s+/g, " ").toLowerCase();
3574
+ /** Address evidence identity excludes refinements such as unit, floor and region. */
3318
3575
  function sameMeaningfulAddress(a, b) {
3319
3576
  if (a === null || b === null) return a === b;
3320
3577
  return spatialAddressParts.every((part) => comparisonText(a[part]) === comparisonText(b[part]));
@@ -3322,12 +3579,21 @@ function sameMeaningfulAddress(a, b) {
3322
3579
  function samePin(a, b) {
3323
3580
  return a === null || b === null ? a === b : a.lat === b.lat && a.lng === b.lng;
3324
3581
  }
3582
+ /** Spatial identity ignores unit/floor/region/note edits; picker reply tags share this rule. */
3583
+ function sameLocationSpatialPair(a, b) {
3584
+ return a === null || b === null ? a === b : sameMeaningfulAddress(a.address, b.address) && samePin(a.pin, b.pin);
3585
+ }
3325
3586
  /** Edits cannot carry forward old confirmation or address evidence by accident. */
3326
3587
  function editLocationValue(previous, details) {
3327
3588
  if (details.address === null && details.pin === null) return ok(null);
3589
+ const authored = {
3590
+ address: details.address,
3591
+ pin: details.pin,
3592
+ doorNote: details.doorNote
3593
+ };
3328
3594
  if (previous === null) {
3329
3595
  return parseLocationValue({
3330
- ...details,
3596
+ ...authored,
3331
3597
  addressMatch: null,
3332
3598
  ownerConfirmedPinAndAddress: false
3333
3599
  });
@@ -3335,8 +3601,9 @@ function editLocationValue(previous, details) {
3335
3601
  const sameAddress = sameMeaningfulAddress(previous.address, details.address);
3336
3602
  const samePair = sameAddress && samePin(previous.pin, details.pin);
3337
3603
  return parseLocationValue({
3338
- ...details,
3604
+ ...authored,
3339
3605
  addressMatch: sameAddress ? previous.addressMatch : null,
3606
+ ...sameAddress && previous.addressCheckSettled ? { addressCheckSettled: true } : {},
3340
3607
  ownerConfirmedPinAndAddress: samePair && previous.ownerConfirmedPinAndAddress
3341
3608
  });
3342
3609
  }
@@ -3365,6 +3632,21 @@ function decideLocationSave(location, activity) {
3365
3632
  location
3366
3633
  };
3367
3634
  }
3635
+ /** Opening lookup evidence may change provider facts only, never the owner's saved place. */
3636
+ function planLocationOpeningEvidence(previous, evidence) {
3637
+ const parsed = parseLocationValue(evidence);
3638
+ if (previous === null || !parsed.ok || parsed.value === null) return err({ kind: "invalid_location" });
3639
+ const next = parsed.value;
3640
+ const authored = (value) => ({
3641
+ address: value.address,
3642
+ pin: value.pin,
3643
+ doorNote: value.doorNote,
3644
+ ownerConfirmedPinAndAddress: value.ownerConfirmedPinAndAddress
3645
+ });
3646
+ if (stableJsonStringify(authored(previous)) !== stableJsonStringify(authored(next)) || next.addressMatch === null && next.addressCheckSettled !== true) return err({ kind: "invalid_location" });
3647
+ if (stableJsonStringify(previous) === stableJsonStringify(next)) return ok(next);
3648
+ return previous.addressMatch !== null || previous.addressCheckSettled === true ? err({ kind: "invalid_location" }) : ok(next);
3649
+ }
3368
3650
 
3369
3651
  //#endregion
3370
3652
  //#region ../core/src/location-migration.ts
@@ -3475,17 +3757,25 @@ function mapVenueAddress(address) {
3475
3757
  };
3476
3758
  return Object.values(structured).some((part) => displayComparison(part) !== "") ? structured : { street: firstText(address.formatted, address.summary) };
3477
3759
  }
3478
- function checkDisplayText(result, text) {
3760
+ const displayTokens = (text) => (text ?? "").toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
3761
+ /** Only redundant trailing country text is disposable; ordered address words must all survive. */
3762
+ function comparableDisplay(text, countries) {
3763
+ const tokens = displayTokens(text);
3764
+ const suffixIndex = tokens.findIndex((_token, index) => countries.some((country) => sameLocationCountry(tokens.slice(index).join(" "), country)));
3765
+ return (suffixIndex < 0 ? tokens : tokens.slice(0, suffixIndex)).join(" ");
3766
+ }
3767
+ function checkDisplayText(result, text, siteCountry) {
3479
3768
  if (!result.ok) return result;
3480
3769
  const original = text?.trim() ?? "";
3481
3770
  const derived = formatLocationAddress(result.value?.address ?? null);
3482
- return original.length > 0 && displayComparison(original) !== displayComparison(derived) ? err({
3771
+ const countries = [result.value?.address?.country, siteCountry];
3772
+ return original.length > 0 && comparableDisplay(original, countries) !== comparableDisplay(derived, countries) ? err({
3483
3773
  kind: "display_address_conflict",
3484
3774
  original,
3485
3775
  derived
3486
3776
  }) : result;
3487
3777
  }
3488
- function convertBusiness(value) {
3778
+ function convertBusiness(value, siteCountry) {
3489
3779
  if (value == null) return ok(null);
3490
3780
  const parsed = businessAddressSchema.safeParse(value);
3491
3781
  if (!parsed.success) return err({
@@ -3500,7 +3790,7 @@ function convertBusiness(value) {
3500
3790
  postalCode: parsed.data.postalCode,
3501
3791
  country: parsed.data.countryCode
3502
3792
  }, parsed.data);
3503
- return checkDisplayText(result, parsed.data.displayAddress);
3793
+ return checkDisplayText(result, parsed.data.displayAddress, siteCountry);
3504
3794
  }
3505
3795
  function convertCustom(value) {
3506
3796
  if (value == null) return ok(null);
@@ -3514,7 +3804,7 @@ function convertCustom(value) {
3514
3804
  function convertLegacyLocation(input) {
3515
3805
  switch (input.kind) {
3516
3806
  case "venue": return convertVenue(input);
3517
- case "business": return convertBusiness(input.value);
3807
+ case "business": return convertBusiness(input.value, input.siteCountry);
3518
3808
  case "custom": return convertCustom(input.value);
3519
3809
  }
3520
3810
  }
@@ -3526,7 +3816,7 @@ function normalizeCountryCode(value) {
3526
3816
  return trimmed !== null ? trimmed.toUpperCase() : null;
3527
3817
  }
3528
3818
  function normalizeCoordinate(value) {
3529
- return typeof value === "number" && Number.isFinite(value) ? value : null;
3819
+ return typeof value === "number" ? value : null;
3530
3820
  }
3531
3821
  function isLatitude(value) {
3532
3822
  return value >= -90 && value <= 90;
@@ -3534,7 +3824,7 @@ function isLatitude(value) {
3534
3824
  function isLongitude(value) {
3535
3825
  return value >= -180 && value <= 180;
3536
3826
  }
3537
- function buildBusinessAddressDisplayText(input) {
3827
+ function buildBusinessAddressDisplayText(input, siteCountry) {
3538
3828
  return formatLocationAddress({
3539
3829
  building: null,
3540
3830
  street: trimmedStringOrNull(input.addressLine1),
@@ -3544,7 +3834,7 @@ function buildBusinessAddressDisplayText(input) {
3544
3834
  region: trimmedStringOrNull(input.region),
3545
3835
  postalCode: trimmedStringOrNull(input.postalCode),
3546
3836
  country: normalizeCountryCode(input.countryCode)
3547
- });
3837
+ }, siteCountry);
3548
3838
  }
3549
3839
  function validateBusinessAddressDraft(input) {
3550
3840
  const addressLine1 = trimmedStringOrNull(input.addressLine1);
@@ -3556,16 +3846,13 @@ function validateBusinessAddressDraft(input) {
3556
3846
  const displayAddress = trimmedStringOrNull(input.displayAddress) ?? buildBusinessAddressDisplayText(input);
3557
3847
  const lat = normalizeCoordinate(input.lat);
3558
3848
  const lng = normalizeCoordinate(input.lng);
3559
- const errors = [];
3560
- if (addressLine1 === null) errors.push("missing_address_line_1");
3561
- if (locality === null) errors.push("missing_locality");
3562
- if (countryCode === null) errors.push("missing_country_code");
3563
- if (displayAddress === null) errors.push("missing_display_address");
3564
- if (lat === null) errors.push("missing_latitude");
3565
- else if (!isLatitude(lat)) errors.push("invalid_latitude");
3566
- if (lng === null) errors.push("missing_longitude");
3567
- else if (!isLongitude(lng)) errors.push("invalid_longitude");
3568
- if (errors.length > 0 || addressLine1 === null || locality === null || countryCode === null || displayAddress === null || lat === null || lng === null) {
3849
+ const errors = [...missingPostalFields({
3850
+ addressLine1,
3851
+ locality,
3852
+ countryCode,
3853
+ displayAddress
3854
+ }), ...businessPinErrors(lat, lng)];
3855
+ if (errors.length > 0 || addressLine1 === null || locality === null || countryCode === null || displayAddress === null) {
3569
3856
  return {
3570
3857
  success: false,
3571
3858
  errors
@@ -3598,6 +3885,95 @@ function businessAddressToAddressedLocation(address) {
3598
3885
  lng: address.lng
3599
3886
  };
3600
3887
  }
3888
+ /** Temporary postal/editor response adapter. Canonical storage never uses this shape. */
3889
+ function businessAddressFromLocation(location, siteCountry) {
3890
+ if (location === null || location.address === null) return null;
3891
+ const address = location.address;
3892
+ const displayAddress = formatLocationAddress(address, siteCountry);
3893
+ if (displayAddress === null) return null;
3894
+ const pin = location.pin ?? {
3895
+ lat: null,
3896
+ lng: null
3897
+ };
3898
+ return {
3899
+ addressLine1: [address.building, address.street].filter((part) => part !== null).join(", "),
3900
+ addressLine2: [address.unit, address.floor].filter((part) => part !== null).join(", ") || null,
3901
+ locality: address.locality ?? "",
3902
+ region: address.region,
3903
+ postalCode: address.postalCode,
3904
+ countryCode: address.country ?? "",
3905
+ displayAddress,
3906
+ lat: pin.lat,
3907
+ lng: pin.lng
3908
+ };
3909
+ }
3910
+ /** Legacy editor inputs are explicit write adapters, never fallback storage readers. */
3911
+ function planBusinessLocationWrite(previous, input) {
3912
+ if ("locationValue" in input && input.expectedLocationValue !== undefined) {
3913
+ if (stableJsonStringify(previous) !== stableJsonStringify(input.expectedLocationValue)) return err({ kind: "invalid_location" });
3914
+ return planLocationOpeningEvidence(previous, input.locationValue);
3915
+ }
3916
+ const planned = "locationValue" in input ? parseLocationValue(input.locationValue) : planLegacyBusinessLocation(previous, input.businessAddress);
3917
+ if (!planned.ok) return err({ kind: "locationValue" in input ? "invalid_location" : "invalid_legacy_location" });
3918
+ const decision = decideLocationSave(planned.value, "idle");
3919
+ return decision.kind === "blocked" ? err({ kind: "unconfirmed_mismatch" }) : ok(decision.location);
3920
+ }
3921
+ function planLegacyBusinessLocation(previous, address) {
3922
+ if (address === null) return ok(null);
3923
+ if (previous === null) return convertLegacyBusinessEdit(previous, address);
3924
+ const projection = businessAddressFromLocation(previous);
3925
+ return projection !== null && sameBusinessPostalProjection(projection, address) ? editBusinessPin(previous, address) : convertLegacyBusinessEdit(previous, address);
3926
+ }
3927
+ function editBusinessPin(previous, address) {
3928
+ const pin = locationPinSchema.nullable().safeParse(address.lat === null && address.lng === null ? null : {
3929
+ lat: address.lat,
3930
+ lng: address.lng
3931
+ });
3932
+ return pin.success ? editLocationValue(previous, {
3933
+ address: previous.address,
3934
+ pin: pin.data,
3935
+ doorNote: previous.doorNote
3936
+ }) : err({ kind: "invalid_location" });
3937
+ }
3938
+ function convertLegacyBusinessEdit(previous, address) {
3939
+ const converted = convertLegacyLocation({
3940
+ kind: "business",
3941
+ value: address
3942
+ });
3943
+ if (!converted.ok || converted.value === null) return converted;
3944
+ return editLocationValue(previous, {
3945
+ address: converted.value.address,
3946
+ pin: converted.value.pin,
3947
+ doorNote: previous?.doorNote ?? null
3948
+ });
3949
+ }
3950
+ function sameBusinessPostalProjection(a, b) {
3951
+ const fields = [
3952
+ "addressLine1",
3953
+ "addressLine2",
3954
+ "locality",
3955
+ "region",
3956
+ "postalCode",
3957
+ "countryCode",
3958
+ "displayAddress"
3959
+ ];
3960
+ return fields.every((field) => (a[field] ?? "").trim().replace(/\s+/g, " ").toLowerCase() === (b[field] ?? "").trim().replace(/\s+/g, " ").toLowerCase());
3961
+ }
3962
+ function missingPostalFields(fields) {
3963
+ const required = [
3964
+ [fields.addressLine1, "missing_address_line_1"],
3965
+ [fields.locality, "missing_locality"],
3966
+ [fields.countryCode, "missing_country_code"],
3967
+ [fields.displayAddress, "missing_display_address"]
3968
+ ];
3969
+ return required.flatMap(([value, code]) => value === null ? [code] : []);
3970
+ }
3971
+ function businessPinErrors(lat, lng) {
3972
+ if (lat === null && lng === null) return [];
3973
+ const latitude = lat === null ? ["missing_latitude"] : !isLatitude(lat) ? ["invalid_latitude"] : [];
3974
+ const longitude = lng === null ? ["missing_longitude"] : !isLongitude(lng) ? ["invalid_longitude"] : [];
3975
+ return [...latitude, ...longitude];
3976
+ }
3601
3977
 
3602
3978
  //#endregion
3603
3979
  //#region ../core/src/try-async.ts
@@ -4702,12 +5078,12 @@ function parsePublicSocialLinks(value) {
4702
5078
  });
4703
5079
  continue;
4704
5080
  }
4705
- const label = trimmedOptionalString(rawLink.label);
4706
- if (rawLink.kind === "custom" && label !== null && label !== undefined) {
5081
+ const label$1 = trimmedOptionalString(rawLink.label);
5082
+ if (rawLink.kind === "custom" && label$1 !== null && label$1 !== undefined) {
4707
5083
  links.push({
4708
5084
  id: id.value,
4709
5085
  kind: "custom",
4710
- label,
5086
+ label: label$1,
4711
5087
  url: url.value
4712
5088
  });
4713
5089
  continue;
@@ -5892,10 +6268,10 @@ function parseFooterEngagementSection(value) {
5892
6268
  });
5893
6269
  }
5894
6270
  function upsertFooterEngagementSection(composition, section) {
5895
- const hasEngagement = composition.sections.some((candidate) => candidate.kind === "engagement");
6271
+ const hasEngagement = composition.sections.some((candidate$1) => candidate$1.kind === "engagement");
5896
6272
  return validateFooterComposition({
5897
6273
  ...composition,
5898
- sections: hasEngagement ? composition.sections.map((candidate) => candidate.kind === "engagement" ? section : candidate) : [...composition.sections, section]
6274
+ sections: hasEngagement ? composition.sections.map((candidate$1) => candidate$1.kind === "engagement" ? section : candidate$1) : [...composition.sections, section]
5899
6275
  });
5900
6276
  }
5901
6277
  function removeFooterEngagementSection(composition) {
@@ -6956,9 +7332,9 @@ function selectSubmissionPreviewFields(schemaFields, submissions) {
6956
7332
  if (id === null) return null;
6957
7333
  const type = (field.type ?? "text").toString().toLowerCase();
6958
7334
  if (shouldHideInPreview(type)) return null;
6959
- const label = trimmedStringOrNull(field.label?.toString()) ?? humanizeSubmissionFieldKey(id);
7335
+ const label$1 = trimmedStringOrNull(field.label?.toString()) ?? humanizeSubmissionFieldKey(id);
6960
7336
  const normalizedId = id.toLowerCase();
6961
- const normalizedLabel = label.toLowerCase();
7337
+ const normalizedLabel = label$1.toLowerCase();
6962
7338
  let score = 0;
6963
7339
  if (PRIORITY_IDS.has(normalizedId)) score += 500;
6964
7340
  if (normalizedId.includes("name") || normalizedLabel.includes("name")) score += 550;
@@ -6981,7 +7357,7 @@ function selectSubmissionPreviewFields(schemaFields, submissions) {
6981
7357
  }
6982
7358
  return {
6983
7359
  id,
6984
- label,
7360
+ label: label$1,
6985
7361
  type,
6986
7362
  score,
6987
7363
  index
@@ -7514,33 +7890,6 @@ function generateUuidV4(cryptoSource = globalThis.crypto) {
7514
7890
  return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
7515
7891
  }
7516
7892
 
7517
- //#endregion
7518
- //#region ../core/src/stable-json.ts
7519
- function stableJsonStringify(value) {
7520
- return JSON.stringify(sortStableJsonValue(value));
7521
- }
7522
- function sortStableJsonValue(value) {
7523
- if (value === null) return null;
7524
- if (typeof value === "string" || typeof value === "boolean") return value;
7525
- if (typeof value === "number") {
7526
- if (!Number.isFinite(value)) {
7527
- throw new TypeError("Stable JSON only supports finite numbers");
7528
- }
7529
- return value;
7530
- }
7531
- if (Array.isArray(value)) {
7532
- return value.map(sortStableJsonValue);
7533
- }
7534
- if (typeof value === "object") {
7535
- const prototype = Object.getPrototypeOf(value);
7536
- if (prototype !== Object.prototype && prototype !== null) {
7537
- throw new TypeError("Stable JSON only supports plain objects");
7538
- }
7539
- return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, child]) => [key, sortStableJsonValue(child)]));
7540
- }
7541
- throw new TypeError(`Stable JSON does not support ${typeof value}`);
7542
- }
7543
-
7544
7893
  //#endregion
7545
7894
  //#region ../core/src/secret.ts
7546
7895
  /**
@@ -7943,14 +8292,14 @@ const BOOKING_DEFAULTS_BY_BEHAVIOUR = {
7943
8292
  memberships: "off"
7944
8293
  }
7945
8294
  };
7946
- function verticalBookingPreset(id, verticalId, canonicalBehaviour, label, pluralLabel, description, availability$1 = "onboarding") {
8295
+ function verticalBookingPreset(id, verticalId, canonicalBehaviour, label$1, pluralLabel, description, availability$1 = "onboarding") {
7947
8296
  return {
7948
8297
  id,
7949
8298
  verticalId,
7950
8299
  canonicalBehaviour,
7951
8300
  engine: BOOKING_ENGINE_BY_BEHAVIOUR[canonicalBehaviour],
7952
8301
  availability: availability$1,
7953
- label,
8302
+ label: label$1,
7954
8303
  pluralLabel,
7955
8304
  description
7956
8305
  };
@@ -8826,6 +9175,616 @@ function resolvePublicSiteVisibility(input) {
8826
9175
  return "live";
8827
9176
  }
8828
9177
 
9178
+ //#endregion
9179
+ //#region ../core/src/map-drawing/geometry.ts
9180
+ const VIEW_BOUNDS = {
9181
+ left: 0,
9182
+ top: 0,
9183
+ right: 800,
9184
+ bottom: 800
9185
+ };
9186
+ function samePoint(a, b) {
9187
+ return a[0] === b[0] && a[1] === b[1];
9188
+ }
9189
+ function distance(a, b) {
9190
+ return Math.hypot(a[0] - b[0], a[1] - b[1]);
9191
+ }
9192
+ function roundCoordinate(value) {
9193
+ const rounded = Math.round(value * 10) / 10;
9194
+ return rounded === 0 ? 0 : rounded;
9195
+ }
9196
+ function roundPoint(point$1) {
9197
+ return [roundCoordinate(point$1[0]), roundCoordinate(point$1[1])];
9198
+ }
9199
+ /** Liang–Barsky: one bounded segment, no fabricated connecting line across the viewport. */
9200
+ function clipSegment(a, b, bounds) {
9201
+ const dx = b[0] - a[0], dy = b[1] - a[1];
9202
+ const edges = [
9203
+ [-dx, a[0] - bounds.left],
9204
+ [dx, bounds.right - a[0]],
9205
+ [-dy, a[1] - bounds.top],
9206
+ [dy, bounds.bottom - a[1]]
9207
+ ];
9208
+ const interval = edges.reduce((range, [p, q]) => {
9209
+ if (range === null || p === 0 && q < 0) return null;
9210
+ if (p === 0) return range;
9211
+ const ratio = q / p;
9212
+ const next = p < 0 ? [Math.max(range[0], ratio), range[1]] : [range[0], Math.min(range[1], ratio)];
9213
+ return next[0] > next[1] ? null : next;
9214
+ }, [0, 1]);
9215
+ return interval === null ? null : [[a[0] + interval[0] * dx, a[1] + interval[0] * dy], [a[0] + interval[1] * dx, a[1] + interval[1] * dy]];
9216
+ }
9217
+ function segments(points, closed = false) {
9218
+ return points.flatMap((start, index) => {
9219
+ const end = points[index + 1] ?? (closed ? points[0] : undefined);
9220
+ return end === undefined ? [] : [{
9221
+ start,
9222
+ end
9223
+ }];
9224
+ });
9225
+ }
9226
+ function clipLine(points, bounds) {
9227
+ return segments(points).reduce((paths$1, { start, end }) => {
9228
+ const segment = clipSegment(start, end, bounds);
9229
+ if (segment === null || samePoint(segment[0], segment[1])) return paths$1;
9230
+ const previous = paths$1[paths$1.length - 1];
9231
+ const last = previous?.[previous.length - 1];
9232
+ return previous !== undefined && last !== undefined && samePoint(last, segment[0]) ? [...paths$1.slice(0, -1), [...previous, segment[1]]] : [...paths$1, segment];
9233
+ }, []);
9234
+ }
9235
+ function polygonEdges(bounds) {
9236
+ const vertical = (x) => (a, b) => [x, a[1] + (b[1] - a[1]) * (x - a[0]) / (b[0] - a[0])];
9237
+ const horizontal = (y) => (a, b) => [a[0] + (b[0] - a[0]) * (y - a[1]) / (b[1] - a[1]), y];
9238
+ return [
9239
+ {
9240
+ inside: (p) => p[0] >= bounds.left,
9241
+ cross: vertical(bounds.left)
9242
+ },
9243
+ {
9244
+ inside: (p) => p[0] <= bounds.right,
9245
+ cross: vertical(bounds.right)
9246
+ },
9247
+ {
9248
+ inside: (p) => p[1] >= bounds.top,
9249
+ cross: horizontal(bounds.top)
9250
+ },
9251
+ {
9252
+ inside: (p) => p[1] <= bounds.bottom,
9253
+ cross: horizontal(bounds.bottom)
9254
+ }
9255
+ ];
9256
+ }
9257
+ function clipPolygonEdge(points, edge) {
9258
+ return segments(points, true).flatMap(({ start, end }) => {
9259
+ const wasInside = edge.inside(start), isInside = edge.inside(end);
9260
+ if (wasInside && isInside) return [end];
9261
+ if (wasInside) return [edge.cross(start, end)];
9262
+ return isInside ? [edge.cross(start, end), end] : [];
9263
+ });
9264
+ }
9265
+ function clipPolygon(points, bounds) {
9266
+ const first = points[0], last = points[points.length - 1];
9267
+ const open = first !== undefined && last !== undefined && samePoint(first, last) ? points.slice(0, -1) : points;
9268
+ return polygonEdges(bounds).reduce((ring, edge) => clipPolygonEdge(ring, edge), open);
9269
+ }
9270
+ function deviation(point$1, start, end) {
9271
+ const dx = end[0] - start[0], dy = end[1] - start[1];
9272
+ const fraction = Math.max(0, Math.min(1, ((point$1[0] - start[0]) * dx + (point$1[1] - start[1]) * dy) / (dx * dx + dy * dy || 1)));
9273
+ return distance(point$1, [start[0] + fraction * dx, start[1] + fraction * dy]);
9274
+ }
9275
+ function simplifyChunk(points) {
9276
+ const start = points[0], end = points[points.length - 1];
9277
+ if (start === undefined || end === undefined || points.length <= 2) return points;
9278
+ const furthest = points.slice(1, -1).reduce((best, point$1, index) => {
9279
+ const error = deviation(point$1, start, end);
9280
+ return error > best.error ? {
9281
+ error,
9282
+ index: index + 1
9283
+ } : best;
9284
+ }, {
9285
+ error: .8,
9286
+ index: -1
9287
+ });
9288
+ return furthest.index < 0 ? [start, end] : [...simplifyChunk(points.slice(0, furthest.index + 1)).slice(0, -1), ...simplifyChunk(points.slice(furthest.index))];
9289
+ }
9290
+ /** Douglas–Peucker in overlapping 256-point chunks bounds recursion/work on hostile zigzags. */
9291
+ function simplify(points, closed) {
9292
+ const rounded = points.map(roundPoint);
9293
+ const open = rounded.filter((point$1, index) => rounded[index - 1] === undefined || !samePoint(point$1, rounded[index - 1] ?? point$1));
9294
+ const first = open[0], last = open[open.length - 1];
9295
+ if (first === undefined || last === undefined) return [];
9296
+ const input = closed && !samePoint(first, last) ? [...open, first] : open;
9297
+ const kept = Array.from({ length: Math.ceil((input.length - 1) / 255) }, (_, index) => simplifyChunk(input.slice(index * 255, index * 255 + 256))).flatMap((chunk, index) => index === 0 ? chunk : chunk.slice(1));
9298
+ const start = kept[0], end = kept[kept.length - 1];
9299
+ return closed && start !== undefined && end !== undefined && samePoint(start, end) ? kept.slice(0, -1) : kept;
9300
+ }
9301
+ function expandBounds(bounds, by) {
9302
+ return {
9303
+ left: bounds.left - by,
9304
+ top: bounds.top - by,
9305
+ right: bounds.right + by,
9306
+ bottom: bounds.bottom + by
9307
+ };
9308
+ }
9309
+ function polygonArea(points) {
9310
+ return Math.abs(segments(points, true).reduce((area, { start, end }) => area + start[0] * end[1] - end[0] * start[1], 0)) / 2;
9311
+ }
9312
+
9313
+ //#endregion
9314
+ //#region ../core/src/map-drawing/projection.ts
9315
+ const EARTH_CIRCUMFERENCE = 2 * Math.PI * 6378137;
9316
+ const MERCATOR_LIMIT = 85.0511287798066;
9317
+ function createProjection(pin, radius) {
9318
+ const latitude = pin.lat * Math.PI / 180;
9319
+ const center = [(pin.lng + 180) / 360, (1 - Math.asinh(Math.tan(latitude)) / Math.PI) / 2];
9320
+ const span = 2 * radius / (EARTH_CIRCUMFERENCE * Math.cos(latitude));
9321
+ return {
9322
+ center,
9323
+ span,
9324
+ project: (tile$1, point$1) => {
9325
+ const x = (tile$1.x + point$1[0] / tile$1.extent) / 2 ** tile$1.z;
9326
+ const y = (tile$1.y + point$1[1] / tile$1.extent) / 2 ** tile$1.z;
9327
+ const wrapped = (x - center[0] + 1.5) % 1 - .5;
9328
+ return [400 + wrapped / span * 800, 400 + (y - center[1]) / span * 800];
9329
+ }
9330
+ };
9331
+ }
9332
+ function tileBounds(tile$1, projection) {
9333
+ const a = projection.project(tile$1, [0, 0]), b = projection.project(tile$1, [tile$1.extent, tile$1.extent]);
9334
+ return {
9335
+ left: a[0],
9336
+ top: a[1],
9337
+ right: b[0],
9338
+ bottom: b[1]
9339
+ };
9340
+ }
9341
+ function supportsProjection(pin, projection) {
9342
+ return Math.abs(pin.lat) <= MERCATOR_LIMIT && projection.center[1] - projection.span / 2 >= 0 && projection.center[1] + projection.span / 2 <= 1;
9343
+ }
9344
+
9345
+ //#endregion
9346
+ //#region ../core/src/map-drawing/features.ts
9347
+ const compareKey = (a, b) => a < b ? -1 : a > b ? 1 : 0;
9348
+ const greenKinds = [
9349
+ "park",
9350
+ "forest",
9351
+ "wood",
9352
+ "grass",
9353
+ "meadow",
9354
+ "garden",
9355
+ "nature_reserve",
9356
+ "protected_area",
9357
+ "recreation_ground",
9358
+ "village_green"
9359
+ ];
9360
+ function polygonRole(feature$1) {
9361
+ if (feature$1.layer === "earth") return "map-land";
9362
+ if (feature$1.layer === "water") return feature$1.kind === "swimming_pool" ? null : "map-water";
9363
+ if (feature$1.layer === "buildings") return "map-building";
9364
+ return feature$1.layer === "landuse" && greenKinds.includes(feature$1.kind) ? "map-park" : null;
9365
+ }
9366
+ function roadRole(kind) {
9367
+ if (kind === "highway" || kind === "major_road") return "major";
9368
+ if (kind === "minor_road") return "minor";
9369
+ return kind === "path" ? "path" : null;
9370
+ }
9371
+ function intersection(a, b) {
9372
+ const bounds = {
9373
+ left: Math.max(a.left, b.left),
9374
+ top: Math.max(a.top, b.top),
9375
+ right: Math.min(a.right, b.right),
9376
+ bottom: Math.min(a.bottom, b.bottom)
9377
+ };
9378
+ return bounds.left >= bounds.right || bounds.top >= bounds.bottom ? null : bounds;
9379
+ }
9380
+ function canonicalRing(points) {
9381
+ const head = points[0];
9382
+ if (head === undefined) return [];
9383
+ const first = points.reduce((best, point$1, index) => point$1[0] < best.point[0] || point$1[0] === best.point[0] && point$1[1] < best.point[1] ? {
9384
+ point: point$1,
9385
+ index
9386
+ } : best, {
9387
+ point: head,
9388
+ index: 0
9389
+ });
9390
+ const forward = [...points.slice(first.index), ...points.slice(0, first.index)];
9391
+ const backward = [first.point, ...forward.slice(1).reverse()];
9392
+ return compareKey(JSON.stringify(forward), JSON.stringify(backward)) <= 0 ? forward : backward;
9393
+ }
9394
+ function canonicalLine(points) {
9395
+ const reversed = points.slice().reverse();
9396
+ return compareKey(JSON.stringify(points), JSON.stringify(reversed)) <= 0 ? points : reversed;
9397
+ }
9398
+ const CENTRE = [400, 400];
9399
+ function pinDistance(geometry) {
9400
+ if (geometry.kind === "road") return segments(geometry.points).reduce((nearest, { start, end }) => Math.min(nearest, deviation(CENTRE, start, end)), Infinity);
9401
+ return geometry.rings.reduce((best, ring) => ring.reduce((nearest, point$1) => Math.min(nearest, distance(point$1, CENTRE)), best), Infinity);
9402
+ }
9403
+ function candidate(geometry, rank) {
9404
+ const key = JSON.stringify(geometry.kind === "polygon" ? geometry : {
9405
+ kind: "road",
9406
+ role: geometry.role,
9407
+ points: geometry.points
9408
+ });
9409
+ return {
9410
+ geometry,
9411
+ rank,
9412
+ key,
9413
+ distance: pinDistance(geometry)
9414
+ };
9415
+ }
9416
+ /**
9417
+ * Polygons are clipped a fraction past their tile edge so neighbouring tiles' fills overlap; clipped exactly,
9418
+ * anti-aliasing leaves a hairline of background along every tile seam. Fills must be opaque for this to hold.
9419
+ */
9420
+ const SEAM_OVERLAP = .5;
9421
+ function projectFeature(tile$1, feature$1, projection) {
9422
+ const bounds = intersection(expandBounds(tileBounds(tile$1, projection), feature$1.geometry === "polygon" ? SEAM_OVERLAP : 0), VIEW_BOUNDS);
9423
+ if (bounds === null) return [];
9424
+ const paths$1 = feature$1.paths.map((path) => path.map((point$1) => projection.project(tile$1, point$1)));
9425
+ if (feature$1.geometry === "line") {
9426
+ const role = roadRole(feature$1.kind);
9427
+ return role ? paths$1.flatMap((path) => clipLine(path, bounds)).map((path) => canonicalLine(simplify(canonicalLine(path), false))).filter((path) => path.length >= 2 && segments(path).reduce((sum, { start, end }) => sum + distance(start, end), 0) >= 1).map((points) => candidate({
9428
+ kind: "road",
9429
+ role,
9430
+ points,
9431
+ name: feature$1.name
9432
+ }, feature$1.sortRank)) : [];
9433
+ }
9434
+ const className = polygonRole(feature$1);
9435
+ if (!className) return [];
9436
+ const rings = paths$1.map((path) => simplify(canonicalRing(clipPolygon(path, bounds)), true)).filter((ring) => ring.length >= 3 && polygonArea(ring) >= .3).map(canonicalRing).slice().sort((a, b) => compareKey(JSON.stringify(a), JSON.stringify(b)));
9437
+ return rings.length ? [candidate({
9438
+ kind: "polygon",
9439
+ fillRule: "evenodd",
9440
+ className,
9441
+ rings
9442
+ }, feature$1.sortRank)] : [];
9443
+ }
9444
+ function deduplicate(candidates) {
9445
+ return candidates.slice().sort((a, b) => compareKey(a.key, b.key) || a.rank - b.rank || compareKey(JSON.stringify(a.geometry), JSON.stringify(b.geometry))).filter((item, index, sorted) => item.key !== sorted[index - 1]?.key);
9446
+ }
9447
+
9448
+ //#endregion
9449
+ //#region ../core/src/map-drawing/labels.ts
9450
+ /**
9451
+ * Collision budget for a ~12.5-unit label in an ordinary sans-serif, with room for a heavier site font.
9452
+ * Renderers can set label type smaller than this, never larger.
9453
+ */
9454
+ const CHARACTER_WIDTH = 7;
9455
+ const HALF_HEIGHT = 8;
9456
+ const PADDING = 5;
9457
+ /** How far a street may bend away from the straight line its label is written along. */
9458
+ const MAX_BEND = 4.5;
9459
+ /** Preferred places along a street for its label, as fractions of its visible length. */
9460
+ const POSITIONS = [
9461
+ .5,
9462
+ .4,
9463
+ .6,
9464
+ .3,
9465
+ .7,
9466
+ .2,
9467
+ .8,
9468
+ .1,
9469
+ .9
9470
+ ];
9471
+ /** Street pieces whose ends are this close are one street (OSM ways and tile clipping both split streets). */
9472
+ const JOIN_DISTANCE = 1;
9473
+ /**
9474
+ * Every label lies wholly inside the central 400 × 300 units. Renderers always show at least that much (a phone
9475
+ * at 0.85 px per unit or more, a 1200 × 500 band at 1.5), so a crop never cuts a street name in half.
9476
+ */
9477
+ const LABEL_WINDOW = {
9478
+ left: 200,
9479
+ right: 600,
9480
+ top: 250,
9481
+ bottom: 550
9482
+ };
9483
+ /** The pin is a teardrop 26 units wide and 38 tall with its tip on the point; labels keep clear of it. */
9484
+ const PIN_KEEP_OUT = {
9485
+ left: 382,
9486
+ right: 418,
9487
+ top: 358,
9488
+ bottom: 406
9489
+ };
9490
+ const LABELLED_ROLES = ["major", "minor"];
9491
+ const textLength = (text) => Array.from(text).length * CHARACTER_WIDTH;
9492
+ /** The label's rotated rectangle, which collision checks treat as solid. */
9493
+ function corners(label$1) {
9494
+ const radians = label$1.angle * Math.PI / 180, cos = Math.cos(radians), sin = Math.sin(radians);
9495
+ const half = textLength(label$1.text) / 2 + PADDING;
9496
+ const at = (x, y) => [label$1.point[0] + x * cos - y * sin, label$1.point[1] + x * sin + y * cos];
9497
+ return [
9498
+ at(-half, -HALF_HEIGHT),
9499
+ at(half, -HALF_HEIGHT),
9500
+ at(half, HALF_HEIGHT),
9501
+ at(-half, HALF_HEIGHT)
9502
+ ];
9503
+ }
9504
+ const boxCorners = (box) => [
9505
+ [box.left, box.top],
9506
+ [box.right, box.top],
9507
+ [box.right, box.bottom],
9508
+ [box.left, box.bottom]
9509
+ ];
9510
+ /** The label's axis-aligned extent once rotated. */
9511
+ function labelBounds(label$1) {
9512
+ const points = corners(label$1), xs = points.map((point$1) => point$1[0]), ys = points.map((point$1) => point$1[1]);
9513
+ return {
9514
+ left: Math.min(...xs),
9515
+ right: Math.max(...xs),
9516
+ top: Math.min(...ys),
9517
+ bottom: Math.max(...ys)
9518
+ };
9519
+ }
9520
+ function overlaps(a, b) {
9521
+ return a.left <= b.right && a.right >= b.left && a.top <= b.bottom && a.bottom >= b.top;
9522
+ }
9523
+ /** Separating-axis test for two rectangles; diagonal labels would block far too much as bounding boxes. */
9524
+ function rectanglesMeet(a, b) {
9525
+ const axes = [a, b].flatMap((box) => [[box[1][0] - box[0][0], box[1][1] - box[0][1]], [box[3][0] - box[0][0], box[3][1] - box[0][1]]]);
9526
+ return axes.every(([x, y]) => {
9527
+ const project = (box) => box.map((point$1) => point$1[0] * x + point$1[1] * y);
9528
+ const first = project(a), second = project(b);
9529
+ return Math.max(...first) >= Math.min(...second) && Math.max(...second) >= Math.min(...first);
9530
+ });
9531
+ }
9532
+ function labelsCollide(a, b) {
9533
+ return rectanglesMeet(corners(a), corners(b));
9534
+ }
9535
+ function labelCoversPin(label$1) {
9536
+ return rectanglesMeet(corners(label$1), boxCorners(PIN_KEEP_OUT));
9537
+ }
9538
+ const inside = (inner, outer) => inner.left >= outer.left && inner.right <= outer.right && inner.top >= outer.top && inner.bottom <= outer.bottom;
9539
+ function pointAlong(points, at) {
9540
+ return segments(points).reduce((state, { start, end }) => {
9541
+ if (state.point !== null) return state;
9542
+ const size = distance(start, end);
9543
+ if (size === 0 || state.remaining > size) return {
9544
+ remaining: state.remaining - size,
9545
+ point: null
9546
+ };
9547
+ const fraction = state.remaining / size;
9548
+ return {
9549
+ remaining: 0,
9550
+ point: [start[0] + (end[0] - start[0]) * fraction, start[1] + (end[1] - start[1]) * fraction]
9551
+ };
9552
+ }, {
9553
+ remaining: at,
9554
+ point: null
9555
+ }).point;
9556
+ }
9557
+ /** The run of street a label of `size` would cover around `centre`, with the vertices inside it. */
9558
+ function stretch(points, centre, size) {
9559
+ const start = pointAlong(points, centre - size / 2), end = pointAlong(points, centre + size / 2), middle = pointAlong(points, centre);
9560
+ if (start === null || end === null || middle === null) return null;
9561
+ const offsets = points.reduce((sums, point$1, index) => [...sums, index === 0 ? 0 : (sums[index - 1] ?? 0) + distance(points[index - 1] ?? point$1, point$1)], []);
9562
+ return {
9563
+ start,
9564
+ end,
9565
+ middle,
9566
+ vertices: points.filter((_, index) => Math.abs((offsets[index] ?? 0) - centre) < size / 2)
9567
+ };
9568
+ }
9569
+ function uprightAngle(start, end) {
9570
+ const degrees = Math.atan2(end[1] - start[1], end[0] - start[0]) * 180 / Math.PI;
9571
+ const upright = degrees > 90 ? degrees - 180 : degrees <= -90 ? degrees + 180 : degrees;
9572
+ return Math.round(upright * 10) / 10 || 0;
9573
+ }
9574
+ /** Joins `line` onto `chain` when one end of each meets, keeping the chain's direction where it can. */
9575
+ function join(chain, line$1) {
9576
+ const meets = (a, b) => a !== undefined && b !== undefined && distance(a, b) <= JOIN_DISTANCE;
9577
+ const reversed = line$1.slice().reverse();
9578
+ if (meets(chain[chain.length - 1], line$1[0])) return [...chain, ...line$1.slice(1)];
9579
+ if (meets(chain[chain.length - 1], reversed[0])) return [...chain, ...reversed.slice(1)];
9580
+ if (meets(line$1[line$1.length - 1], chain[0])) return [...line$1, ...chain.slice(1)];
9581
+ return meets(reversed[reversed.length - 1], chain[0]) ? [...reversed, ...chain.slice(1)] : null;
9582
+ }
9583
+ function stitchPass(streets) {
9584
+ return streets.reduce((chains, street) => {
9585
+ const index = chains.findIndex((chain$1) => chain$1.name === street.name && chain$1.major === street.major && join(chain$1.points, street.points) !== null);
9586
+ const chain = chains[index];
9587
+ const joined = chain === undefined ? null : join(chain.points, street.points);
9588
+ return chain === undefined || joined === null ? [...chains, street] : chains.map((item, at) => at === index ? {
9589
+ ...item,
9590
+ distance: Math.min(item.distance, street.distance),
9591
+ points: joined
9592
+ } : item);
9593
+ }, []);
9594
+ }
9595
+ /** Whole named streets from their pieces, so a name is written along the street rather than one short way of it. */
9596
+ function stitch(streets) {
9597
+ const next = stitchPass(streets);
9598
+ return next.length === streets.length ? next : stitch(next);
9599
+ }
9600
+ function namedStreets(roads) {
9601
+ return roads.slice().sort((a, b) => compareKey(a.key, b.key)).flatMap(({ geometry, distance: distance$1 }) => geometry.kind === "road" && geometry.name !== null && LABELLED_ROLES.includes(geometry.role) ? [{
9602
+ name: geometry.name,
9603
+ major: geometry.role === "major",
9604
+ distance: distance$1,
9605
+ points: geometry.points
9606
+ }] : []);
9607
+ }
9608
+ /** Label options for one street, best first: only where the street runs straight enough to write along. */
9609
+ function streetLabels({ name: text, points }) {
9610
+ const size = textLength(text);
9611
+ const length = segments(points).reduce((sum, { start, end }) => sum + distance(start, end), 0);
9612
+ if (length < size) return [];
9613
+ return POSITIONS.flatMap((position) => {
9614
+ const centre = Math.min(Math.max(length * position, size / 2), length - size / 2);
9615
+ const run = stretch(points, centre, size);
9616
+ if (run === null || run.vertices.some((vertex) => deviation(vertex, run.start, run.end) > MAX_BEND)) return [];
9617
+ return [{
9618
+ className: "map-label",
9619
+ haloClassName: "map-label-halo",
9620
+ text,
9621
+ point: roundPoint(run.middle),
9622
+ angle: uprightAngle(run.start, run.end)
9623
+ }];
9624
+ });
9625
+ }
9626
+ function fits(label$1, placed) {
9627
+ return inside(labelBounds(label$1), LABEL_WINDOW) && !labelCoversPin(label$1) && placed.every((other) => other.text !== label$1.text && !labelsCollide(label$1, other));
9628
+ }
9629
+ /** Nearest streets first, as the framing weighs them: the ones around the door are the names a visitor needs. */
9630
+ function placeLabels(roads, framing) {
9631
+ const { maxLabels, minorLabelDistance } = framing;
9632
+ const weighted = (street) => street.major ? street.distance : street.distance * minorLabelDistance;
9633
+ return stitch(namedStreets(roads)).slice().sort((a, b) => weighted(a) - weighted(b) || compareKey(a.name, b.name)).reduce((placed, street) => {
9634
+ if (placed.length >= maxLabels) return placed;
9635
+ const label$1 = streetLabels(street).find((option) => fits(option, placed));
9636
+ return label$1 === undefined ? placed : [...placed, label$1];
9637
+ }, []);
9638
+ }
9639
+
9640
+ //#endregion
9641
+ //#region ../core/src/map-drawing/coverage.ts
9642
+ /** The bounded mixed-zoom coverage decision shared by extraction and completeness validation. */
9643
+ function drawingTileCoverage(pin) {
9644
+ const contexts = FRAMINGS.map((framing) => ({
9645
+ framing,
9646
+ projection: createProjection(pin, framing.radiusMeters)
9647
+ }));
9648
+ if (contexts.some(({ projection }) => !supportsProjection(pin, projection))) return err("unsupported-latitude");
9649
+ const bounds = contexts.map(({ framing, projection }) => {
9650
+ const n = 2 ** framing.zoom;
9651
+ const left = Math.floor((projection.center[0] - projection.span / 2) * n), right = Math.floor((projection.center[0] + projection.span / 2) * n);
9652
+ const top = Math.floor((projection.center[1] - projection.span / 2) * n), bottom = Math.floor((projection.center[1] + projection.span / 2) * n);
9653
+ return {
9654
+ z: framing.zoom,
9655
+ n,
9656
+ left,
9657
+ top,
9658
+ bottom,
9659
+ width: right - left + 1,
9660
+ count: (right - left + 1) * (bottom - top + 1)
9661
+ };
9662
+ });
9663
+ if (bounds.some((value) => !Number.isFinite(value.count) || value.count < 1 || value.top < 0 || value.bottom >= value.n) || bounds.reduce((sum, value) => sum + value.count, 0) > 96) return err("geometry-limit");
9664
+ return ok(bounds.flatMap(({ z: z$1, n, left, top, width, count }) => Array.from({ length: count }, (_, index) => ({
9665
+ z: z$1,
9666
+ x: ((left + index % width) % n + n) % n,
9667
+ y: top + Math.floor(index / width)
9668
+ }))));
9669
+ }
9670
+
9671
+ //#endregion
9672
+ //#region ../core/src/map-drawing/generate.ts
9673
+ const ROLE_ORDER = [
9674
+ "major",
9675
+ "minor",
9676
+ "path"
9677
+ ];
9678
+ const MAX_BUILDINGS = 1e3;
9679
+ const MAX_ROADS = 300;
9680
+ /** What this framing draws: its road roles, buildings or not, and no green patches too small to read. */
9681
+ function belongsIn(framing, item) {
9682
+ const geometry = item.geometry;
9683
+ if (geometry.kind === "road") return framing.roads.some((role) => role === geometry.role) && (geometry.role !== "path" || item.distance <= framing.maxPathDistance);
9684
+ if (geometry.className === "map-building") return framing.buildings;
9685
+ return geometry.className !== "map-park" || geometry.rings.some((ring) => polygonArea(ring) >= framing.minParkArea);
9686
+ }
9687
+ function selectFeatures(framing, candidates) {
9688
+ const ordered = candidates.filter((item) => belongsIn(framing, item)).sort((a, b) => a.distance - b.distance || a.rank - b.rank || compareKey(a.key, b.key));
9689
+ const buildings = ordered.filter((item) => item.geometry.kind === "polygon" && item.geometry.className === "map-building").slice(0, MAX_BUILDINGS);
9690
+ const roleOrder = (item) => item.geometry.kind === "road" ? ROLE_ORDER.indexOf(item.geometry.role) : 0;
9691
+ const roads = ordered.filter((item) => item.geometry.kind === "road").sort((a, b) => roleOrder(a) - roleOrder(b) || a.distance - b.distance || compareKey(a.key, b.key)).slice(0, MAX_ROADS);
9692
+ const ground = ordered.filter((item) => item.geometry.kind === "polygon" && item.geometry.className !== "map-building");
9693
+ return [
9694
+ ...ground,
9695
+ ...buildings,
9696
+ ...roads
9697
+ ];
9698
+ }
9699
+ /** Water over green, so a wood or park running to the bank never hides the river. */
9700
+ const POLYGON_ORDER = [
9701
+ "map-land",
9702
+ "map-park",
9703
+ "map-water",
9704
+ "map-building"
9705
+ ];
9706
+ function paintingOrder(candidate$1) {
9707
+ const geometry = candidate$1.geometry;
9708
+ return geometry.kind === "road" ? POLYGON_ORDER.length + ROLE_ORDER.length - ROLE_ORDER.indexOf(geometry.role) : POLYGON_ORDER.indexOf(geometry.className);
9709
+ }
9710
+ const casingClass = {
9711
+ major: "map-road-major-casing",
9712
+ minor: "map-road-minor-casing"
9713
+ };
9714
+ const strokeClass = {
9715
+ major: "map-road-major",
9716
+ minor: "map-road-minor",
9717
+ path: "map-road-path"
9718
+ };
9719
+ /** Casings under every stroke; paths are a single thin line with no casing. */
9720
+ function roadPaths(roads) {
9721
+ const lines = roads.flatMap((item) => item.geometry.kind === "road" ? [item.geometry] : []);
9722
+ const casings = lines.flatMap(({ role, points }) => role === "path" ? [] : [{
9723
+ kind: "line",
9724
+ className: casingClass[role],
9725
+ points
9726
+ }]);
9727
+ return [...casings, ...lines.map(({ role, points }) => ({
9728
+ kind: "line",
9729
+ className: strokeClass[role],
9730
+ points
9731
+ }))];
9732
+ }
9733
+ function generateFrame(input, framing, projection) {
9734
+ const candidates = input.tiles.filter((tile$1) => tile$1.z === framing.zoom).flatMap((tile$1) => tile$1.features.flatMap((feature$1) => projectFeature(tile$1, feature$1, projection)));
9735
+ const selected = selectFeatures(framing, deduplicate(candidates)).slice().sort((a, b) => paintingOrder(a) - paintingOrder(b) || a.rank - b.rank || compareKey(a.key, b.key));
9736
+ const polygons = selected.flatMap((item) => item.geometry.kind === "polygon" ? [item.geometry] : []);
9737
+ const roads = selected.filter((item) => item.geometry.kind === "road");
9738
+ return {
9739
+ radiusMeters: framing.radiusMeters,
9740
+ viewBox: [
9741
+ 0,
9742
+ 0,
9743
+ 800,
9744
+ 800
9745
+ ],
9746
+ backgroundClass: "map-water",
9747
+ paths: [...polygons, ...roadPaths(roads)],
9748
+ labels: placeLabels(roads, framing),
9749
+ pin: {
9750
+ className: "map-pin",
9751
+ point: [400, 400]
9752
+ }
9753
+ };
9754
+ }
9755
+ /** Pure: parse decoded input once, decide both framings, return only closed numeric geometry. */
9756
+ function generateMapDrawing(raw) {
9757
+ const parsed = mapDrawingInputSchema.safeParse(raw);
9758
+ if (!parsed.success) return err("invalid-input");
9759
+ const input = parsed.data;
9760
+ const coverage = drawingTileCoverage(input.pin);
9761
+ if (!coverage.ok) return err(coverage.error);
9762
+ const neighbourhood = {
9763
+ framing: FRAMINGS[0],
9764
+ projection: createProjection(input.pin, FRAMINGS[0].radiusMeters)
9765
+ };
9766
+ const wider = {
9767
+ framing: FRAMINGS[1],
9768
+ projection: createProjection(input.pin, FRAMINGS[1].radiusMeters)
9769
+ };
9770
+ const keys = input.tiles.map((tile$1) => `${tile$1.z}/${tile$1.x}/${tile$1.y}`);
9771
+ const present = new Set(keys);
9772
+ if (present.size !== keys.length) return err("invalid-input");
9773
+ if (coverage.value.some((tile$1) => !present.has(`${tile$1.z}/${tile$1.x}/${tile$1.y}`))) return err("missing-tiles");
9774
+ const drawing = mapDrawingSchema.safeParse({
9775
+ schemaVersion: DRAWING_SCHEMA_VERSION,
9776
+ generatorVersion: DRAWING_GENERATOR_VERSION,
9777
+ buildId: input.buildId,
9778
+ tileSchemaVersion: input.tileSchemaVersion,
9779
+ frames: {
9780
+ neighbourhood: generateFrame(input, neighbourhood.framing, neighbourhood.projection),
9781
+ wider: generateFrame(input, wider.framing, wider.projection)
9782
+ }
9783
+ });
9784
+ if (!drawing.success) return err("geometry-limit");
9785
+ return ok(drawing.data);
9786
+ }
9787
+
8829
9788
  //#endregion
8830
9789
  //#region ../core/src/authored-values.ts
8831
9790
  /** Canonical authored equality: empty storage values and absent optional members agree. */
@@ -8855,4 +9814,4 @@ function isPlainRecord(value) {
8855
9814
  }
8856
9815
 
8857
9816
  //#endregion
8858
- export { inferSiteDateLocale as $, parseCurrencyCode as $t, assessMailingListTargeting as A, unsafeAsPositiveByteCount as An, resolveFlexibleBalanceInitialPaymentCents as At, parsePublicContactDetails as B, unsafeAsStoragePath as Bn, resolveExactOrFreePriceDisplay as Bt, parseManagementKey as C, unsafeAsEventOfferingId as Cn, clampConfiguredAmount as Ct, parseMailingListContentVersionId as D, unsafeAsOccurrenceId as Dn, omitZeroAmountFutureRows as Dt, parseMailingListBlockId as E, unsafeAsMediaAssetId as En, getBookingInstalmentProtectedDepositAmountCents as Et, parseMailingListPlacementTargeting as F, unsafeAsSiteFontId as Fn, isPublicEventPricingOptionDisplayable as Ft, isPresent as G, isNonNegativeWholeCents as Gt, assertAllowedPublicUrl as H, unsafeAsUserId as Hn, fromArray as Ht, DEFAULT_MAILING_LIST_TARGETING as I, unsafeAsSiteId as In, publicEventPricingCurrency as It, formatDateRange as J, toCents as Jt, selectCategoryView as K, parseNonNegativeCents as Kt, planMailingListChoicePresentation as L, unsafeAsSiteScopedStoragePath as Ln, publicEventPricingDisplay as Lt, parseMailingListProviderScope as M, unsafeAsPricingOptionId as Mn, parseEventPageSurfaceSubject as Mt, parseFooterCompositionV1 as N, unsafeAsProductId as Nn, formatBookingPriceDisplay as Nt, parseMailingListSignedSourceToken as O, unsafeAsOriginalFilename as On, splitAmountWithRemainderInFirst as Ot, footerNavigationLayouts as P, unsafeAsSafeDownloadFilename as Pn, formatEventPricing as Pt, formatTimeRange as Q, formatCurrencyAmount as Qt, isCanonicalRichTextDoc as R, unsafeAsStaffMemberId as Rn, publicEventPricingOptions as Rt, exposeSecret as S, unsafeAsEventInstanceId as Sn, addIntervalPreservingMonthAnchor as St, slugify as T, unsafeAsFileChecksumSha256 as Tn, getBookingInstalmentPlanIntervalCount as Tt, normalizeLocationAddressText as U, err as Un, mapNonEmptyArray as Ut, parseWeeklyOpeningHours as V, unsafeAsUploadSessionId as Vn, resolvePricingOptionsPriceDisplay as Vt, toLocationCoordinates as W, ok as Wn, mapReadonlyNonEmptyArray as Wt, formatDateTimeRange as X, unsafeAsNonNegativeCents as Xt, formatDateTime as Y, toNonNegativeCents as Yt, formatTime as Z, unsafeAsPositiveCents as Zt, nextLiveRunSessionStartMs as _, unsafeAsCheckoutLineId as _n, parseBookingTimeZone as _t, areConfiguredBookingVerticalConfigsEqual as a, parseDatedOfferingCapacityHoldId as an, getBookingLocalDateKey as at, resolveWholeSeriesRunLifecycle as b, unsafeAsDetectedMimeType as bn, isSettleableFlexibleBalanceContractStatus as bt, parseVerticalPresetId as c, parseMembershipId as cn, localScheduleTimePattern as ct, EVENT_PRESET_VALUES as d, parseSiteId as dn, parseUtcIsoDateTimeString as dt, assertNever as en, resolveBookingDisplayContext as et, ORDINARY_EVENT_PRESET_VALUES as f, parseSiteTermsVersionId as fn, resolveBookingBusinessTimeZone as ft, CANCELLED_SESSION_STATUS as g, unsafeAsBookingSessionId as gn, toUtcIsoDateTimeString as gt, publicReadFreshnessSeconds as h, unsafeAsBookingRequirementId as hn, toScheduledBookingDateTime as ht, NAVIGATION_LOCATIONS as i, parseDatedOfferingCapacityHoldAuditRef as in, formatLocalScheduleTime as it, DEFAULT_MAILING_LIST_SUCCESS_MESSAGE as j, unsafeAsPositiveSeconds as jn, deriveTotalTicketQuantity as jt, parseMailingListRuntimeContext as k, unsafeAsPassId as kn, resolveFlexibleBalanceDueAt as kt, resolveSiteVerticalBookingConfig as l, parsePageId as ln, parseBookingLocalDate as lt, WHOLE_SERIES_EVENT_PRESET_VALUES as m, unsafeAsAppointmentResourceId as mn, toBookingTimeZone as mt, resolveEventCatalogue as n, parseApiKeyId as nn, compareLocalScheduleDates as nt, canonicalizeConfiguredBookingVerticalConfig as o, parseEventSeriesId as on, getLocalScheduleDate as ot, SURFACE_EVENT_PRESET_VALUES as p, parseUserId as pn, resolveBookingTimeZoneId as pt, formatDate as q, parsePositiveCents as qt, EVENT_PRESET_VOCABULARY as r, parseCustomerId as rn, formatLocalScheduleDate as rt, parseVerticalId as s, parseMediaAssetId as sn, getLocalScheduleTime as st, authoredValuesEqual as t, formatDurationUnitLabel as tn, buildBookingUtcDateWindow as tt, DEFAULT_EVENT_TYPE_PRESETS as u, parseRetiredCourseId as un, parseLocalScheduleTime as ut, orderedLiveRunSessions as v, unsafeAsDeclaredMimeType as vn, validateBookingPaymentOptionKinds as vt, stableJsonStringify as w, unsafeAsEventSeriesId as wn, getBookingInstalmentDepositRefundability as wt, createSecret as x, unsafeAsEntitlementId as xn, isSettleableOrderInstalmentPlanStatus as xt, resolveWholeSeriesBookingPhase as y, unsafeAsDemoCapability as yn, isLiveOrderInstalmentPlanStatus as yt, formatOpeningHoursRows as z, unsafeAsStorageBucketName as zn, publicEventPricingType as zt };
9817
+ export { selectCategoryView as $, parseNonNegativeCents as $t, DEFAULT_MAILING_LIST_SUCCESS_MESSAGE as A, unsafeAsEventOfferingId as An, clampConfiguredAmount as At, parseWeeklyOpeningHours as B, unsafeAsProductId as Bn, formatBookingPriceDisplay as Bt, parseManagementKey as C, unsafeAsBookingSessionId as Cn, toUtcIsoDateTimeString as Ct, parseMailingListSignedSourceToken as D, unsafeAsDetectedMimeType as Dn, isSettleableFlexibleBalanceContractStatus as Dt, parseMailingListContentVersionId as E, unsafeAsDemoCapability as En, isLiveOrderInstalmentPlanStatus as Et, DEFAULT_MAILING_LIST_TARGETING as F, unsafeAsOriginalFilename as Fn, splitAmountWithRemainderInFirst as Ft, locationValueSchema as G, unsafeAsStaffMemberId as Gn, publicEventPricingOptions as Gt, decideLocationSave as H, unsafeAsSiteFontId as Hn, isPublicEventPricingOptionDisplayable as Ht, planMailingListChoicePresentation as I, unsafeAsPassId as In, resolveFlexibleBalanceDueAt as It, projectPublicLocation as J, unsafeAsUploadSessionId as Jn, resolvePricingOptionsPriceDisplay as Jt, normalizeLocationAddressText as K, unsafeAsStorageBucketName as Kn, publicEventPricingType as Kt, isCanonicalRichTextDoc as L, unsafeAsPositiveByteCount as Ln, resolveFlexibleBalanceInitialPaymentCents as Lt, parseFooterCompositionV1 as M, unsafeAsFileChecksumSha256 as Mn, getBookingInstalmentPlanIntervalCount as Mt, footerNavigationLayouts as N, unsafeAsMediaAssetId as Nn, getBookingInstalmentProtectedDepositAmountCents as Nt, parseMailingListRuntimeContext as O, unsafeAsEntitlementId as On, isSettleableOrderInstalmentPlanStatus as Ot, parseMailingListPlacementTargeting as P, unsafeAsOccurrenceId as Pn, omitZeroAmountFutureRows as Pt, mapDrawingSchema as Q, isNonNegativeWholeCents as Qt, formatOpeningHoursRows as R, unsafeAsPositiveSeconds as Rn, deriveTotalTicketQuantity as Rt, exposeSecret as S, unsafeAsBookingRequirementId as Sn, toScheduledBookingDateTime as St, parseMailingListBlockId as T, unsafeAsDeclaredMimeType as Tn, validateBookingPaymentOptionKinds as Tt, stableJsonStringify as U, unsafeAsSiteId as Un, publicEventPricingCurrency as Ut, assertAllowedPublicUrl as V, unsafeAsSafeDownloadFilename as Vn, formatEventPricing as Vt, locationMapLinks as W, unsafeAsSiteScopedStoragePath as Wn, publicEventPricingDisplay as Wt, toLocationCoordinates as X, err as Xn, mapNonEmptyArray as Xt, publicLocationSchema as Y, unsafeAsUserId as Yn, fromArray as Yt, isPresent as Z, ok as Zn, mapReadonlyNonEmptyArray as Zt, nextLiveRunSessionStartMs as _, parseRetiredCourseId as _n, parseLocalScheduleTime as _t, areConfiguredBookingVerticalConfigsEqual as a, formatCurrencyAmount as an, formatTimeRange as at, resolveWholeSeriesRunLifecycle as b, parseUserId as bn, resolveBookingTimeZoneId as bt, parseVerticalPresetId as c, formatDurationUnitLabel as cn, buildBookingUtcDateWindow as ct, EVENT_PRESET_VALUES as d, parseDatedOfferingCapacityHoldAuditRef as dn, formatLocalScheduleTime as dt, parsePositiveCents as en, formatDate as et, ORDINARY_EVENT_PRESET_VALUES as f, parseDatedOfferingCapacityHoldId as fn, getBookingLocalDateKey as ft, CANCELLED_SESSION_STATUS as g, parsePageId as gn, parseBookingLocalDate as gt, publicReadFreshnessSeconds as h, parseMembershipId as hn, localScheduleTimePattern as ht, NAVIGATION_LOCATIONS as i, unsafeAsPositiveCents as in, formatTime as it, parseMailingListProviderScope as j, unsafeAsEventSeriesId as jn, getBookingInstalmentDepositRefundability as jt, assessMailingListTargeting as k, unsafeAsEventInstanceId as kn, addIntervalPreservingMonthAnchor as kt, resolveSiteVerticalBookingConfig as l, parseApiKeyId as ln, compareLocalScheduleDates as lt, WHOLE_SERIES_EVENT_PRESET_VALUES as m, parseMediaAssetId as mn, getLocalScheduleTime as mt, resolveEventCatalogue as n, toNonNegativeCents as nn, formatDateTime as nt, canonicalizeConfiguredBookingVerticalConfig as o, parseCurrencyCode as on, inferSiteDateLocale as ot, SURFACE_EVENT_PRESET_VALUES as p, parseEventSeriesId as pn, getLocalScheduleDate as pt, parseLocationValue as q, unsafeAsStoragePath as qn, resolveExactOrFreePriceDisplay as qt, EVENT_PRESET_VOCABULARY as r, unsafeAsNonNegativeCents as rn, formatDateTimeRange as rt, parseVerticalId as s, assertNever as sn, resolveBookingDisplayContext as st, authoredValuesEqual as t, toCents as tn, formatDateRange as tt, DEFAULT_EVENT_TYPE_PRESETS as u, parseCustomerId as un, formatLocalScheduleDate as ut, orderedLiveRunSessions as v, parseSiteId as vn, parseUtcIsoDateTimeString as vt, slugify as w, unsafeAsCheckoutLineId as wn, parseBookingTimeZone as wt, createSecret as x, unsafeAsAppointmentResourceId as xn, toBookingTimeZone as xt, resolveWholeSeriesBookingPhase as y, parseSiteTermsVersionId as yn, resolveBookingBusinessTimeZone as yt, parsePublicContactDetails as z, unsafeAsPricingOptionId as zn, parseEventPageSurfaceSubject as zt };