riverbankcms 0.23.0 → 0.25.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.
@@ -52,7 +52,7 @@ const unsafeAsPageId = (id) => id;
52
52
  const unsafeAsPageSurfaceId = (id) => id;
53
53
  const unsafeAsEditLeaseToken = (id) => id;
54
54
  const unsafeAsEditorClientId = (id) => id;
55
- const unsafeAsPageDocumentCheckpointId = (id) => id;
55
+ const unsafeAsEditorSessionGeneration = (id) => id;
56
56
  const unsafeAsOccurrenceId = (id) => id;
57
57
  const unsafeAsEventOfferingId = (id) => id;
58
58
  const unsafeAsEventSeriesId = (id) => id;
@@ -167,7 +167,7 @@ const parsePageId = (value) => parseUuidBrand(value, unsafeAsPageId);
167
167
  const parsePageSurfaceId = (value) => parseUuidBrand(value, unsafeAsPageSurfaceId);
168
168
  const parseEditLeaseToken = (value) => parseUuidBrand(value, unsafeAsEditLeaseToken);
169
169
  const parseEditorClientId = (value) => parseUuidBrand(value, unsafeAsEditorClientId);
170
- const parsePageDocumentCheckpointId = (value) => parseUuidBrand(value, unsafeAsPageDocumentCheckpointId);
170
+ const parseEditorSessionGeneration = (value) => parseUuidBrand(value, unsafeAsEditorSessionGeneration);
171
171
  const parseOccurrenceId = (value) => parseUuidBrand(value, unsafeAsOccurrenceId);
172
172
  const parseEventOfferingId = (value) => parseUuidBrand(value, unsafeAsEventOfferingId);
173
173
  const parseEventAttendeeTicketLineId = (value) => parseUuidBrand(value, unsafeAsEventAttendeeTicketLineId);
@@ -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
@@ -3718,16 +4094,8 @@ function syncStateOf(sync) {
3718
4094
  const EDIT_LEASE_TTL_SECONDS = 60;
3719
4095
  /** One-use navigation transport for the same previous-client acquire contract. */
3720
4096
  const EDIT_LEASE_HANDOVER_QUERY_PARAM = "edit_lease_handover";
3721
- /** Frozen at editor mount; reacquiring a lease never advances this session's admission stamp. */
3722
- const PAGE_DOCUMENT_SESSION_CHECKPOINT_HEADER = "X-Riverbank-Page-Checkpoint";
3723
- function parsePageDocumentSessionCheckpoint(value) {
3724
- if (value === "legacy") return { kind: "legacy" };
3725
- const parsed = parsePageDocumentCheckpointId(value);
3726
- return parsed.ok ? {
3727
- kind: "document",
3728
- checkpointId: parsed.value
3729
- } : undefined;
3730
- }
4097
+ /** Frozen at editor mount; reacquiring a lease never advances the server-issued generation. */
4098
+ const EDITOR_SESSION_GENERATION_HEADER = "X-Riverbank-Editor-Session-Generation";
3731
4099
  function parseEditLeaseScope(input) {
3732
4100
  switch (input.nodeType) {
3733
4101
  case "page": {
@@ -4702,12 +5070,12 @@ function parsePublicSocialLinks(value) {
4702
5070
  });
4703
5071
  continue;
4704
5072
  }
4705
- const label = trimmedOptionalString(rawLink.label);
4706
- if (rawLink.kind === "custom" && label !== null && label !== undefined) {
5073
+ const label$1 = trimmedOptionalString(rawLink.label);
5074
+ if (rawLink.kind === "custom" && label$1 !== null && label$1 !== undefined) {
4707
5075
  links.push({
4708
5076
  id: id.value,
4709
5077
  kind: "custom",
4710
- label,
5078
+ label: label$1,
4711
5079
  url: url.value
4712
5080
  });
4713
5081
  continue;
@@ -5892,10 +6260,10 @@ function parseFooterEngagementSection(value) {
5892
6260
  });
5893
6261
  }
5894
6262
  function upsertFooterEngagementSection(composition, section) {
5895
- const hasEngagement = composition.sections.some((candidate) => candidate.kind === "engagement");
6263
+ const hasEngagement = composition.sections.some((candidate$1) => candidate$1.kind === "engagement");
5896
6264
  return validateFooterComposition({
5897
6265
  ...composition,
5898
- sections: hasEngagement ? composition.sections.map((candidate) => candidate.kind === "engagement" ? section : candidate) : [...composition.sections, section]
6266
+ sections: hasEngagement ? composition.sections.map((candidate$1) => candidate$1.kind === "engagement" ? section : candidate$1) : [...composition.sections, section]
5899
6267
  });
5900
6268
  }
5901
6269
  function removeFooterEngagementSection(composition) {
@@ -6161,6 +6529,36 @@ function planMailingListSetupSwitch(input) {
6161
6529
  affectedPlacementCount: input.placementModes.length
6162
6530
  };
6163
6531
  }
6532
+ const preparedSetupWrite = Symbol("prepared-mailing-list-setup-write");
6533
+ /** The current setup and real placement census decide the only permitted write shape. */
6534
+ function prepareMailingListSetupWrite(input, current, placementModes) {
6535
+ const revision = current.tag === "not_configured" ? null : current.revision;
6536
+ if (revision !== input.expectedRevision || current.siteId !== input.siteId) return { kind: "revision-conflict" };
6537
+ const switchPlan = planMailingListSetupSwitch({
6538
+ previousScope: current.tag === "configured" ? mailingListProviderScopeFromConfigured(current.provider) : null,
6539
+ nextScope: mailingListProviderScopeFromConfigured(input.provider),
6540
+ placementModes,
6541
+ defaultChanged: current.tag !== "configured" || !sameMailingListProviderDefault(current.provider, input.provider)
6542
+ });
6543
+ const plan = {
6544
+ ...input,
6545
+ expectedRevision: revision ?? 0,
6546
+ kind: switchPlan.tag === "replace_scope" ? "reset" : "update",
6547
+ [preparedSetupWrite]: true
6548
+ };
6549
+ Object.defineProperty(plan, preparedSetupWrite, { enumerable: false });
6550
+ return {
6551
+ kind: "prepared",
6552
+ plan
6553
+ };
6554
+ }
6555
+ function sameMailingListProviderDefault(previous, next) {
6556
+ switch (previous.provider) {
6557
+ case "native": return next.provider === "native" && sameNewsletterAudience(previous.defaultAudience, next.defaultAudience);
6558
+ case "brevo": return next.provider === "brevo" && previous.defaultListId === next.defaultListId;
6559
+ case "mailchimp": return next.provider === "mailchimp" && previous.defaultAudienceId === next.defaultAudienceId;
6560
+ }
6561
+ }
6164
6562
  function mailingListProviderScopeFromConfigured(provider) {
6165
6563
  switch (provider.provider) {
6166
6564
  case "native": return { provider: "native" };
@@ -6501,10 +6899,14 @@ function assessCustomTargeting(catalog, targeting) {
6501
6899
  }
6502
6900
  }
6503
6901
  }
6902
+ /** Shared choice availability decision for both runtime catalogues and captured document facts. */
6903
+ function missingMailingListTargetingChoiceIds(targeting, choices) {
6904
+ const available = new Set(choices.map((choice) => choice.id));
6905
+ return mailingListTargetingChoiceIds(targeting).filter((id) => !available.has(id));
6906
+ }
6504
6907
  function assessMatchedChoices(matched, choices) {
6505
6908
  const selected = new Set(mailingListTargetingChoiceIds(matched.targeting));
6506
- const available = new Set(choices.map((item) => item.id));
6507
- const missingIds = [...selected].filter((id) => !available.has(id));
6909
+ const missingIds = missingMailingListTargetingChoiceIds(matched.targeting, choices);
6508
6910
  const value = {
6509
6911
  mode: "custom",
6510
6912
  ...matched,
@@ -6956,9 +7358,9 @@ function selectSubmissionPreviewFields(schemaFields, submissions) {
6956
7358
  if (id === null) return null;
6957
7359
  const type = (field.type ?? "text").toString().toLowerCase();
6958
7360
  if (shouldHideInPreview(type)) return null;
6959
- const label = trimmedStringOrNull(field.label?.toString()) ?? humanizeSubmissionFieldKey(id);
7361
+ const label$1 = trimmedStringOrNull(field.label?.toString()) ?? humanizeSubmissionFieldKey(id);
6960
7362
  const normalizedId = id.toLowerCase();
6961
- const normalizedLabel = label.toLowerCase();
7363
+ const normalizedLabel = label$1.toLowerCase();
6962
7364
  let score = 0;
6963
7365
  if (PRIORITY_IDS.has(normalizedId)) score += 500;
6964
7366
  if (normalizedId.includes("name") || normalizedLabel.includes("name")) score += 550;
@@ -6981,7 +7383,7 @@ function selectSubmissionPreviewFields(schemaFields, submissions) {
6981
7383
  }
6982
7384
  return {
6983
7385
  id,
6984
- label,
7386
+ label: label$1,
6985
7387
  type,
6986
7388
  score,
6987
7389
  index
@@ -7514,33 +7916,6 @@ function generateUuidV4(cryptoSource = globalThis.crypto) {
7514
7916
  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
7917
  }
7516
7918
 
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
7919
  //#endregion
7545
7920
  //#region ../core/src/secret.ts
7546
7921
  /**
@@ -7943,14 +8318,14 @@ const BOOKING_DEFAULTS_BY_BEHAVIOUR = {
7943
8318
  memberships: "off"
7944
8319
  }
7945
8320
  };
7946
- function verticalBookingPreset(id, verticalId, canonicalBehaviour, label, pluralLabel, description, availability$1 = "onboarding") {
8321
+ function verticalBookingPreset(id, verticalId, canonicalBehaviour, label$1, pluralLabel, description, availability$1 = "onboarding") {
7947
8322
  return {
7948
8323
  id,
7949
8324
  verticalId,
7950
8325
  canonicalBehaviour,
7951
8326
  engine: BOOKING_ENGINE_BY_BEHAVIOUR[canonicalBehaviour],
7952
8327
  availability: availability$1,
7953
- label,
8328
+ label: label$1,
7954
8329
  pluralLabel,
7955
8330
  description
7956
8331
  };
@@ -8826,6 +9201,616 @@ function resolvePublicSiteVisibility(input) {
8826
9201
  return "live";
8827
9202
  }
8828
9203
 
9204
+ //#endregion
9205
+ //#region ../core/src/map-drawing/geometry.ts
9206
+ const VIEW_BOUNDS = {
9207
+ left: 0,
9208
+ top: 0,
9209
+ right: 800,
9210
+ bottom: 800
9211
+ };
9212
+ function samePoint(a, b) {
9213
+ return a[0] === b[0] && a[1] === b[1];
9214
+ }
9215
+ function distance(a, b) {
9216
+ return Math.hypot(a[0] - b[0], a[1] - b[1]);
9217
+ }
9218
+ function roundCoordinate(value) {
9219
+ const rounded = Math.round(value * 10) / 10;
9220
+ return rounded === 0 ? 0 : rounded;
9221
+ }
9222
+ function roundPoint(point$1) {
9223
+ return [roundCoordinate(point$1[0]), roundCoordinate(point$1[1])];
9224
+ }
9225
+ /** Liang–Barsky: one bounded segment, no fabricated connecting line across the viewport. */
9226
+ function clipSegment(a, b, bounds) {
9227
+ const dx = b[0] - a[0], dy = b[1] - a[1];
9228
+ const edges = [
9229
+ [-dx, a[0] - bounds.left],
9230
+ [dx, bounds.right - a[0]],
9231
+ [-dy, a[1] - bounds.top],
9232
+ [dy, bounds.bottom - a[1]]
9233
+ ];
9234
+ const interval = edges.reduce((range, [p, q]) => {
9235
+ if (range === null || p === 0 && q < 0) return null;
9236
+ if (p === 0) return range;
9237
+ const ratio = q / p;
9238
+ const next = p < 0 ? [Math.max(range[0], ratio), range[1]] : [range[0], Math.min(range[1], ratio)];
9239
+ return next[0] > next[1] ? null : next;
9240
+ }, [0, 1]);
9241
+ return interval === null ? null : [[a[0] + interval[0] * dx, a[1] + interval[0] * dy], [a[0] + interval[1] * dx, a[1] + interval[1] * dy]];
9242
+ }
9243
+ function segments(points, closed = false) {
9244
+ return points.flatMap((start, index) => {
9245
+ const end = points[index + 1] ?? (closed ? points[0] : undefined);
9246
+ return end === undefined ? [] : [{
9247
+ start,
9248
+ end
9249
+ }];
9250
+ });
9251
+ }
9252
+ function clipLine(points, bounds) {
9253
+ return segments(points).reduce((paths$1, { start, end }) => {
9254
+ const segment = clipSegment(start, end, bounds);
9255
+ if (segment === null || samePoint(segment[0], segment[1])) return paths$1;
9256
+ const previous = paths$1[paths$1.length - 1];
9257
+ const last = previous?.[previous.length - 1];
9258
+ return previous !== undefined && last !== undefined && samePoint(last, segment[0]) ? [...paths$1.slice(0, -1), [...previous, segment[1]]] : [...paths$1, segment];
9259
+ }, []);
9260
+ }
9261
+ function polygonEdges(bounds) {
9262
+ const vertical = (x) => (a, b) => [x, a[1] + (b[1] - a[1]) * (x - a[0]) / (b[0] - a[0])];
9263
+ const horizontal = (y) => (a, b) => [a[0] + (b[0] - a[0]) * (y - a[1]) / (b[1] - a[1]), y];
9264
+ return [
9265
+ {
9266
+ inside: (p) => p[0] >= bounds.left,
9267
+ cross: vertical(bounds.left)
9268
+ },
9269
+ {
9270
+ inside: (p) => p[0] <= bounds.right,
9271
+ cross: vertical(bounds.right)
9272
+ },
9273
+ {
9274
+ inside: (p) => p[1] >= bounds.top,
9275
+ cross: horizontal(bounds.top)
9276
+ },
9277
+ {
9278
+ inside: (p) => p[1] <= bounds.bottom,
9279
+ cross: horizontal(bounds.bottom)
9280
+ }
9281
+ ];
9282
+ }
9283
+ function clipPolygonEdge(points, edge) {
9284
+ return segments(points, true).flatMap(({ start, end }) => {
9285
+ const wasInside = edge.inside(start), isInside = edge.inside(end);
9286
+ if (wasInside && isInside) return [end];
9287
+ if (wasInside) return [edge.cross(start, end)];
9288
+ return isInside ? [edge.cross(start, end), end] : [];
9289
+ });
9290
+ }
9291
+ function clipPolygon(points, bounds) {
9292
+ const first = points[0], last = points[points.length - 1];
9293
+ const open = first !== undefined && last !== undefined && samePoint(first, last) ? points.slice(0, -1) : points;
9294
+ return polygonEdges(bounds).reduce((ring, edge) => clipPolygonEdge(ring, edge), open);
9295
+ }
9296
+ function deviation(point$1, start, end) {
9297
+ const dx = end[0] - start[0], dy = end[1] - start[1];
9298
+ 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)));
9299
+ return distance(point$1, [start[0] + fraction * dx, start[1] + fraction * dy]);
9300
+ }
9301
+ function simplifyChunk(points) {
9302
+ const start = points[0], end = points[points.length - 1];
9303
+ if (start === undefined || end === undefined || points.length <= 2) return points;
9304
+ const furthest = points.slice(1, -1).reduce((best, point$1, index) => {
9305
+ const error = deviation(point$1, start, end);
9306
+ return error > best.error ? {
9307
+ error,
9308
+ index: index + 1
9309
+ } : best;
9310
+ }, {
9311
+ error: .8,
9312
+ index: -1
9313
+ });
9314
+ return furthest.index < 0 ? [start, end] : [...simplifyChunk(points.slice(0, furthest.index + 1)).slice(0, -1), ...simplifyChunk(points.slice(furthest.index))];
9315
+ }
9316
+ /** Douglas–Peucker in overlapping 256-point chunks bounds recursion/work on hostile zigzags. */
9317
+ function simplify(points, closed) {
9318
+ const rounded = points.map(roundPoint);
9319
+ const open = rounded.filter((point$1, index) => rounded[index - 1] === undefined || !samePoint(point$1, rounded[index - 1] ?? point$1));
9320
+ const first = open[0], last = open[open.length - 1];
9321
+ if (first === undefined || last === undefined) return [];
9322
+ const input = closed && !samePoint(first, last) ? [...open, first] : open;
9323
+ 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));
9324
+ const start = kept[0], end = kept[kept.length - 1];
9325
+ return closed && start !== undefined && end !== undefined && samePoint(start, end) ? kept.slice(0, -1) : kept;
9326
+ }
9327
+ function expandBounds(bounds, by) {
9328
+ return {
9329
+ left: bounds.left - by,
9330
+ top: bounds.top - by,
9331
+ right: bounds.right + by,
9332
+ bottom: bounds.bottom + by
9333
+ };
9334
+ }
9335
+ function polygonArea(points) {
9336
+ return Math.abs(segments(points, true).reduce((area, { start, end }) => area + start[0] * end[1] - end[0] * start[1], 0)) / 2;
9337
+ }
9338
+
9339
+ //#endregion
9340
+ //#region ../core/src/map-drawing/projection.ts
9341
+ const EARTH_CIRCUMFERENCE = 2 * Math.PI * 6378137;
9342
+ const MERCATOR_LIMIT = 85.0511287798066;
9343
+ function createProjection(pin, radius) {
9344
+ const latitude = pin.lat * Math.PI / 180;
9345
+ const center = [(pin.lng + 180) / 360, (1 - Math.asinh(Math.tan(latitude)) / Math.PI) / 2];
9346
+ const span = 2 * radius / (EARTH_CIRCUMFERENCE * Math.cos(latitude));
9347
+ return {
9348
+ center,
9349
+ span,
9350
+ project: (tile$1, point$1) => {
9351
+ const x = (tile$1.x + point$1[0] / tile$1.extent) / 2 ** tile$1.z;
9352
+ const y = (tile$1.y + point$1[1] / tile$1.extent) / 2 ** tile$1.z;
9353
+ const wrapped = (x - center[0] + 1.5) % 1 - .5;
9354
+ return [400 + wrapped / span * 800, 400 + (y - center[1]) / span * 800];
9355
+ }
9356
+ };
9357
+ }
9358
+ function tileBounds(tile$1, projection) {
9359
+ const a = projection.project(tile$1, [0, 0]), b = projection.project(tile$1, [tile$1.extent, tile$1.extent]);
9360
+ return {
9361
+ left: a[0],
9362
+ top: a[1],
9363
+ right: b[0],
9364
+ bottom: b[1]
9365
+ };
9366
+ }
9367
+ function supportsProjection(pin, projection) {
9368
+ return Math.abs(pin.lat) <= MERCATOR_LIMIT && projection.center[1] - projection.span / 2 >= 0 && projection.center[1] + projection.span / 2 <= 1;
9369
+ }
9370
+
9371
+ //#endregion
9372
+ //#region ../core/src/map-drawing/features.ts
9373
+ const compareKey = (a, b) => a < b ? -1 : a > b ? 1 : 0;
9374
+ const greenKinds = [
9375
+ "park",
9376
+ "forest",
9377
+ "wood",
9378
+ "grass",
9379
+ "meadow",
9380
+ "garden",
9381
+ "nature_reserve",
9382
+ "protected_area",
9383
+ "recreation_ground",
9384
+ "village_green"
9385
+ ];
9386
+ function polygonRole(feature$1) {
9387
+ if (feature$1.layer === "earth") return "map-land";
9388
+ if (feature$1.layer === "water") return feature$1.kind === "swimming_pool" ? null : "map-water";
9389
+ if (feature$1.layer === "buildings") return "map-building";
9390
+ return feature$1.layer === "landuse" && greenKinds.includes(feature$1.kind) ? "map-park" : null;
9391
+ }
9392
+ function roadRole(kind) {
9393
+ if (kind === "highway" || kind === "major_road") return "major";
9394
+ if (kind === "minor_road") return "minor";
9395
+ return kind === "path" ? "path" : null;
9396
+ }
9397
+ function intersection(a, b) {
9398
+ const bounds = {
9399
+ left: Math.max(a.left, b.left),
9400
+ top: Math.max(a.top, b.top),
9401
+ right: Math.min(a.right, b.right),
9402
+ bottom: Math.min(a.bottom, b.bottom)
9403
+ };
9404
+ return bounds.left >= bounds.right || bounds.top >= bounds.bottom ? null : bounds;
9405
+ }
9406
+ function canonicalRing(points) {
9407
+ const head = points[0];
9408
+ if (head === undefined) return [];
9409
+ 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] ? {
9410
+ point: point$1,
9411
+ index
9412
+ } : best, {
9413
+ point: head,
9414
+ index: 0
9415
+ });
9416
+ const forward = [...points.slice(first.index), ...points.slice(0, first.index)];
9417
+ const backward = [first.point, ...forward.slice(1).reverse()];
9418
+ return compareKey(JSON.stringify(forward), JSON.stringify(backward)) <= 0 ? forward : backward;
9419
+ }
9420
+ function canonicalLine(points) {
9421
+ const reversed = points.slice().reverse();
9422
+ return compareKey(JSON.stringify(points), JSON.stringify(reversed)) <= 0 ? points : reversed;
9423
+ }
9424
+ const CENTRE = [400, 400];
9425
+ function pinDistance(geometry) {
9426
+ if (geometry.kind === "road") return segments(geometry.points).reduce((nearest, { start, end }) => Math.min(nearest, deviation(CENTRE, start, end)), Infinity);
9427
+ return geometry.rings.reduce((best, ring) => ring.reduce((nearest, point$1) => Math.min(nearest, distance(point$1, CENTRE)), best), Infinity);
9428
+ }
9429
+ function candidate(geometry, rank) {
9430
+ const key = JSON.stringify(geometry.kind === "polygon" ? geometry : {
9431
+ kind: "road",
9432
+ role: geometry.role,
9433
+ points: geometry.points
9434
+ });
9435
+ return {
9436
+ geometry,
9437
+ rank,
9438
+ key,
9439
+ distance: pinDistance(geometry)
9440
+ };
9441
+ }
9442
+ /**
9443
+ * Polygons are clipped a fraction past their tile edge so neighbouring tiles' fills overlap; clipped exactly,
9444
+ * anti-aliasing leaves a hairline of background along every tile seam. Fills must be opaque for this to hold.
9445
+ */
9446
+ const SEAM_OVERLAP = .5;
9447
+ function projectFeature(tile$1, feature$1, projection) {
9448
+ const bounds = intersection(expandBounds(tileBounds(tile$1, projection), feature$1.geometry === "polygon" ? SEAM_OVERLAP : 0), VIEW_BOUNDS);
9449
+ if (bounds === null) return [];
9450
+ const paths$1 = feature$1.paths.map((path) => path.map((point$1) => projection.project(tile$1, point$1)));
9451
+ if (feature$1.geometry === "line") {
9452
+ const role = roadRole(feature$1.kind);
9453
+ 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({
9454
+ kind: "road",
9455
+ role,
9456
+ points,
9457
+ name: feature$1.name
9458
+ }, feature$1.sortRank)) : [];
9459
+ }
9460
+ const className = polygonRole(feature$1);
9461
+ if (!className) return [];
9462
+ 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)));
9463
+ return rings.length ? [candidate({
9464
+ kind: "polygon",
9465
+ fillRule: "evenodd",
9466
+ className,
9467
+ rings
9468
+ }, feature$1.sortRank)] : [];
9469
+ }
9470
+ function deduplicate(candidates) {
9471
+ 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);
9472
+ }
9473
+
9474
+ //#endregion
9475
+ //#region ../core/src/map-drawing/labels.ts
9476
+ /**
9477
+ * Collision budget for a ~12.5-unit label in an ordinary sans-serif, with room for a heavier site font.
9478
+ * Renderers can set label type smaller than this, never larger.
9479
+ */
9480
+ const CHARACTER_WIDTH = 7;
9481
+ const HALF_HEIGHT = 8;
9482
+ const PADDING = 5;
9483
+ /** How far a street may bend away from the straight line its label is written along. */
9484
+ const MAX_BEND = 4.5;
9485
+ /** Preferred places along a street for its label, as fractions of its visible length. */
9486
+ const POSITIONS = [
9487
+ .5,
9488
+ .4,
9489
+ .6,
9490
+ .3,
9491
+ .7,
9492
+ .2,
9493
+ .8,
9494
+ .1,
9495
+ .9
9496
+ ];
9497
+ /** Street pieces whose ends are this close are one street (OSM ways and tile clipping both split streets). */
9498
+ const JOIN_DISTANCE = 1;
9499
+ /**
9500
+ * Every label lies wholly inside the central 400 × 300 units. Renderers always show at least that much (a phone
9501
+ * 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.
9502
+ */
9503
+ const LABEL_WINDOW = {
9504
+ left: 200,
9505
+ right: 600,
9506
+ top: 250,
9507
+ bottom: 550
9508
+ };
9509
+ /** The pin is a teardrop 26 units wide and 38 tall with its tip on the point; labels keep clear of it. */
9510
+ const PIN_KEEP_OUT = {
9511
+ left: 382,
9512
+ right: 418,
9513
+ top: 358,
9514
+ bottom: 406
9515
+ };
9516
+ const LABELLED_ROLES = ["major", "minor"];
9517
+ const textLength = (text) => Array.from(text).length * CHARACTER_WIDTH;
9518
+ /** The label's rotated rectangle, which collision checks treat as solid. */
9519
+ function corners(label$1) {
9520
+ const radians = label$1.angle * Math.PI / 180, cos = Math.cos(radians), sin = Math.sin(radians);
9521
+ const half = textLength(label$1.text) / 2 + PADDING;
9522
+ const at = (x, y) => [label$1.point[0] + x * cos - y * sin, label$1.point[1] + x * sin + y * cos];
9523
+ return [
9524
+ at(-half, -HALF_HEIGHT),
9525
+ at(half, -HALF_HEIGHT),
9526
+ at(half, HALF_HEIGHT),
9527
+ at(-half, HALF_HEIGHT)
9528
+ ];
9529
+ }
9530
+ const boxCorners = (box) => [
9531
+ [box.left, box.top],
9532
+ [box.right, box.top],
9533
+ [box.right, box.bottom],
9534
+ [box.left, box.bottom]
9535
+ ];
9536
+ /** The label's axis-aligned extent once rotated. */
9537
+ function labelBounds(label$1) {
9538
+ const points = corners(label$1), xs = points.map((point$1) => point$1[0]), ys = points.map((point$1) => point$1[1]);
9539
+ return {
9540
+ left: Math.min(...xs),
9541
+ right: Math.max(...xs),
9542
+ top: Math.min(...ys),
9543
+ bottom: Math.max(...ys)
9544
+ };
9545
+ }
9546
+ function overlaps(a, b) {
9547
+ return a.left <= b.right && a.right >= b.left && a.top <= b.bottom && a.bottom >= b.top;
9548
+ }
9549
+ /** Separating-axis test for two rectangles; diagonal labels would block far too much as bounding boxes. */
9550
+ function rectanglesMeet(a, b) {
9551
+ 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]]]);
9552
+ return axes.every(([x, y]) => {
9553
+ const project = (box) => box.map((point$1) => point$1[0] * x + point$1[1] * y);
9554
+ const first = project(a), second = project(b);
9555
+ return Math.max(...first) >= Math.min(...second) && Math.max(...second) >= Math.min(...first);
9556
+ });
9557
+ }
9558
+ function labelsCollide(a, b) {
9559
+ return rectanglesMeet(corners(a), corners(b));
9560
+ }
9561
+ function labelCoversPin(label$1) {
9562
+ return rectanglesMeet(corners(label$1), boxCorners(PIN_KEEP_OUT));
9563
+ }
9564
+ const inside = (inner, outer) => inner.left >= outer.left && inner.right <= outer.right && inner.top >= outer.top && inner.bottom <= outer.bottom;
9565
+ function pointAlong(points, at) {
9566
+ return segments(points).reduce((state, { start, end }) => {
9567
+ if (state.point !== null) return state;
9568
+ const size = distance(start, end);
9569
+ if (size === 0 || state.remaining > size) return {
9570
+ remaining: state.remaining - size,
9571
+ point: null
9572
+ };
9573
+ const fraction = state.remaining / size;
9574
+ return {
9575
+ remaining: 0,
9576
+ point: [start[0] + (end[0] - start[0]) * fraction, start[1] + (end[1] - start[1]) * fraction]
9577
+ };
9578
+ }, {
9579
+ remaining: at,
9580
+ point: null
9581
+ }).point;
9582
+ }
9583
+ /** The run of street a label of `size` would cover around `centre`, with the vertices inside it. */
9584
+ function stretch(points, centre, size) {
9585
+ const start = pointAlong(points, centre - size / 2), end = pointAlong(points, centre + size / 2), middle = pointAlong(points, centre);
9586
+ if (start === null || end === null || middle === null) return null;
9587
+ const offsets = points.reduce((sums, point$1, index) => [...sums, index === 0 ? 0 : (sums[index - 1] ?? 0) + distance(points[index - 1] ?? point$1, point$1)], []);
9588
+ return {
9589
+ start,
9590
+ end,
9591
+ middle,
9592
+ vertices: points.filter((_, index) => Math.abs((offsets[index] ?? 0) - centre) < size / 2)
9593
+ };
9594
+ }
9595
+ function uprightAngle(start, end) {
9596
+ const degrees = Math.atan2(end[1] - start[1], end[0] - start[0]) * 180 / Math.PI;
9597
+ const upright = degrees > 90 ? degrees - 180 : degrees <= -90 ? degrees + 180 : degrees;
9598
+ return Math.round(upright * 10) / 10 || 0;
9599
+ }
9600
+ /** Joins `line` onto `chain` when one end of each meets, keeping the chain's direction where it can. */
9601
+ function join(chain, line$1) {
9602
+ const meets = (a, b) => a !== undefined && b !== undefined && distance(a, b) <= JOIN_DISTANCE;
9603
+ const reversed = line$1.slice().reverse();
9604
+ if (meets(chain[chain.length - 1], line$1[0])) return [...chain, ...line$1.slice(1)];
9605
+ if (meets(chain[chain.length - 1], reversed[0])) return [...chain, ...reversed.slice(1)];
9606
+ if (meets(line$1[line$1.length - 1], chain[0])) return [...line$1, ...chain.slice(1)];
9607
+ return meets(reversed[reversed.length - 1], chain[0]) ? [...reversed, ...chain.slice(1)] : null;
9608
+ }
9609
+ function stitchPass(streets) {
9610
+ return streets.reduce((chains, street) => {
9611
+ const index = chains.findIndex((chain$1) => chain$1.name === street.name && chain$1.major === street.major && join(chain$1.points, street.points) !== null);
9612
+ const chain = chains[index];
9613
+ const joined = chain === undefined ? null : join(chain.points, street.points);
9614
+ return chain === undefined || joined === null ? [...chains, street] : chains.map((item, at) => at === index ? {
9615
+ ...item,
9616
+ distance: Math.min(item.distance, street.distance),
9617
+ points: joined
9618
+ } : item);
9619
+ }, []);
9620
+ }
9621
+ /** Whole named streets from their pieces, so a name is written along the street rather than one short way of it. */
9622
+ function stitch(streets) {
9623
+ const next = stitchPass(streets);
9624
+ return next.length === streets.length ? next : stitch(next);
9625
+ }
9626
+ function namedStreets(roads) {
9627
+ 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) ? [{
9628
+ name: geometry.name,
9629
+ major: geometry.role === "major",
9630
+ distance: distance$1,
9631
+ points: geometry.points
9632
+ }] : []);
9633
+ }
9634
+ /** Label options for one street, best first: only where the street runs straight enough to write along. */
9635
+ function streetLabels({ name: text, points }) {
9636
+ const size = textLength(text);
9637
+ const length = segments(points).reduce((sum, { start, end }) => sum + distance(start, end), 0);
9638
+ if (length < size) return [];
9639
+ return POSITIONS.flatMap((position) => {
9640
+ const centre = Math.min(Math.max(length * position, size / 2), length - size / 2);
9641
+ const run = stretch(points, centre, size);
9642
+ if (run === null || run.vertices.some((vertex) => deviation(vertex, run.start, run.end) > MAX_BEND)) return [];
9643
+ return [{
9644
+ className: "map-label",
9645
+ haloClassName: "map-label-halo",
9646
+ text,
9647
+ point: roundPoint(run.middle),
9648
+ angle: uprightAngle(run.start, run.end)
9649
+ }];
9650
+ });
9651
+ }
9652
+ function fits(label$1, placed) {
9653
+ return inside(labelBounds(label$1), LABEL_WINDOW) && !labelCoversPin(label$1) && placed.every((other) => other.text !== label$1.text && !labelsCollide(label$1, other));
9654
+ }
9655
+ /** Nearest streets first, as the framing weighs them: the ones around the door are the names a visitor needs. */
9656
+ function placeLabels(roads, framing) {
9657
+ const { maxLabels, minorLabelDistance } = framing;
9658
+ const weighted = (street) => street.major ? street.distance : street.distance * minorLabelDistance;
9659
+ return stitch(namedStreets(roads)).slice().sort((a, b) => weighted(a) - weighted(b) || compareKey(a.name, b.name)).reduce((placed, street) => {
9660
+ if (placed.length >= maxLabels) return placed;
9661
+ const label$1 = streetLabels(street).find((option) => fits(option, placed));
9662
+ return label$1 === undefined ? placed : [...placed, label$1];
9663
+ }, []);
9664
+ }
9665
+
9666
+ //#endregion
9667
+ //#region ../core/src/map-drawing/coverage.ts
9668
+ /** The bounded mixed-zoom coverage decision shared by extraction and completeness validation. */
9669
+ function drawingTileCoverage(pin) {
9670
+ const contexts = FRAMINGS.map((framing) => ({
9671
+ framing,
9672
+ projection: createProjection(pin, framing.radiusMeters)
9673
+ }));
9674
+ if (contexts.some(({ projection }) => !supportsProjection(pin, projection))) return err("unsupported-latitude");
9675
+ const bounds = contexts.map(({ framing, projection }) => {
9676
+ const n = 2 ** framing.zoom;
9677
+ const left = Math.floor((projection.center[0] - projection.span / 2) * n), right = Math.floor((projection.center[0] + projection.span / 2) * n);
9678
+ const top = Math.floor((projection.center[1] - projection.span / 2) * n), bottom = Math.floor((projection.center[1] + projection.span / 2) * n);
9679
+ return {
9680
+ z: framing.zoom,
9681
+ n,
9682
+ left,
9683
+ top,
9684
+ bottom,
9685
+ width: right - left + 1,
9686
+ count: (right - left + 1) * (bottom - top + 1)
9687
+ };
9688
+ });
9689
+ 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");
9690
+ return ok(bounds.flatMap(({ z: z$1, n, left, top, width, count }) => Array.from({ length: count }, (_, index) => ({
9691
+ z: z$1,
9692
+ x: ((left + index % width) % n + n) % n,
9693
+ y: top + Math.floor(index / width)
9694
+ }))));
9695
+ }
9696
+
9697
+ //#endregion
9698
+ //#region ../core/src/map-drawing/generate.ts
9699
+ const ROLE_ORDER = [
9700
+ "major",
9701
+ "minor",
9702
+ "path"
9703
+ ];
9704
+ const MAX_BUILDINGS = 1e3;
9705
+ const MAX_ROADS = 300;
9706
+ /** What this framing draws: its road roles, buildings or not, and no green patches too small to read. */
9707
+ function belongsIn(framing, item) {
9708
+ const geometry = item.geometry;
9709
+ if (geometry.kind === "road") return framing.roads.some((role) => role === geometry.role) && (geometry.role !== "path" || item.distance <= framing.maxPathDistance);
9710
+ if (geometry.className === "map-building") return framing.buildings;
9711
+ return geometry.className !== "map-park" || geometry.rings.some((ring) => polygonArea(ring) >= framing.minParkArea);
9712
+ }
9713
+ function selectFeatures(framing, candidates) {
9714
+ const ordered = candidates.filter((item) => belongsIn(framing, item)).sort((a, b) => a.distance - b.distance || a.rank - b.rank || compareKey(a.key, b.key));
9715
+ const buildings = ordered.filter((item) => item.geometry.kind === "polygon" && item.geometry.className === "map-building").slice(0, MAX_BUILDINGS);
9716
+ const roleOrder = (item) => item.geometry.kind === "road" ? ROLE_ORDER.indexOf(item.geometry.role) : 0;
9717
+ 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);
9718
+ const ground = ordered.filter((item) => item.geometry.kind === "polygon" && item.geometry.className !== "map-building");
9719
+ return [
9720
+ ...ground,
9721
+ ...buildings,
9722
+ ...roads
9723
+ ];
9724
+ }
9725
+ /** Water over green, so a wood or park running to the bank never hides the river. */
9726
+ const POLYGON_ORDER = [
9727
+ "map-land",
9728
+ "map-park",
9729
+ "map-water",
9730
+ "map-building"
9731
+ ];
9732
+ function paintingOrder(candidate$1) {
9733
+ const geometry = candidate$1.geometry;
9734
+ return geometry.kind === "road" ? POLYGON_ORDER.length + ROLE_ORDER.length - ROLE_ORDER.indexOf(geometry.role) : POLYGON_ORDER.indexOf(geometry.className);
9735
+ }
9736
+ const casingClass = {
9737
+ major: "map-road-major-casing",
9738
+ minor: "map-road-minor-casing"
9739
+ };
9740
+ const strokeClass = {
9741
+ major: "map-road-major",
9742
+ minor: "map-road-minor",
9743
+ path: "map-road-path"
9744
+ };
9745
+ /** Casings under every stroke; paths are a single thin line with no casing. */
9746
+ function roadPaths(roads) {
9747
+ const lines = roads.flatMap((item) => item.geometry.kind === "road" ? [item.geometry] : []);
9748
+ const casings = lines.flatMap(({ role, points }) => role === "path" ? [] : [{
9749
+ kind: "line",
9750
+ className: casingClass[role],
9751
+ points
9752
+ }]);
9753
+ return [...casings, ...lines.map(({ role, points }) => ({
9754
+ kind: "line",
9755
+ className: strokeClass[role],
9756
+ points
9757
+ }))];
9758
+ }
9759
+ function generateFrame(input, framing, projection) {
9760
+ 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)));
9761
+ const selected = selectFeatures(framing, deduplicate(candidates)).slice().sort((a, b) => paintingOrder(a) - paintingOrder(b) || a.rank - b.rank || compareKey(a.key, b.key));
9762
+ const polygons = selected.flatMap((item) => item.geometry.kind === "polygon" ? [item.geometry] : []);
9763
+ const roads = selected.filter((item) => item.geometry.kind === "road");
9764
+ return {
9765
+ radiusMeters: framing.radiusMeters,
9766
+ viewBox: [
9767
+ 0,
9768
+ 0,
9769
+ 800,
9770
+ 800
9771
+ ],
9772
+ backgroundClass: "map-water",
9773
+ paths: [...polygons, ...roadPaths(roads)],
9774
+ labels: placeLabels(roads, framing),
9775
+ pin: {
9776
+ className: "map-pin",
9777
+ point: [400, 400]
9778
+ }
9779
+ };
9780
+ }
9781
+ /** Pure: parse decoded input once, decide both framings, return only closed numeric geometry. */
9782
+ function generateMapDrawing(raw) {
9783
+ const parsed = mapDrawingInputSchema.safeParse(raw);
9784
+ if (!parsed.success) return err("invalid-input");
9785
+ const input = parsed.data;
9786
+ const coverage = drawingTileCoverage(input.pin);
9787
+ if (!coverage.ok) return err(coverage.error);
9788
+ const neighbourhood = {
9789
+ framing: FRAMINGS[0],
9790
+ projection: createProjection(input.pin, FRAMINGS[0].radiusMeters)
9791
+ };
9792
+ const wider = {
9793
+ framing: FRAMINGS[1],
9794
+ projection: createProjection(input.pin, FRAMINGS[1].radiusMeters)
9795
+ };
9796
+ const keys = input.tiles.map((tile$1) => `${tile$1.z}/${tile$1.x}/${tile$1.y}`);
9797
+ const present = new Set(keys);
9798
+ if (present.size !== keys.length) return err("invalid-input");
9799
+ if (coverage.value.some((tile$1) => !present.has(`${tile$1.z}/${tile$1.x}/${tile$1.y}`))) return err("missing-tiles");
9800
+ const drawing = mapDrawingSchema.safeParse({
9801
+ schemaVersion: DRAWING_SCHEMA_VERSION,
9802
+ generatorVersion: DRAWING_GENERATOR_VERSION,
9803
+ buildId: input.buildId,
9804
+ tileSchemaVersion: input.tileSchemaVersion,
9805
+ frames: {
9806
+ neighbourhood: generateFrame(input, neighbourhood.framing, neighbourhood.projection),
9807
+ wider: generateFrame(input, wider.framing, wider.projection)
9808
+ }
9809
+ });
9810
+ if (!drawing.success) return err("geometry-limit");
9811
+ return ok(drawing.data);
9812
+ }
9813
+
8829
9814
  //#endregion
8830
9815
  //#region ../core/src/authored-values.ts
8831
9816
  /** Canonical authored equality: empty storage values and absent optional members agree. */
@@ -8855,4 +9840,468 @@ function isPlainRecord(value) {
8855
9840
  }
8856
9841
 
8857
9842
  //#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 };
9843
+ //#region ../core/src/journal-receipt.ts
9844
+ /** Owners pass captured preimages and committed results; callers never fetch a replacement inverse. */
9845
+ function journalCommitReceipt(target, inverse, revisions) {
9846
+ return {
9847
+ ...target,
9848
+ ...inverse,
9849
+ ...revisions
9850
+ };
9851
+ }
9852
+ function journalReplayValue(inverse, direction) {
9853
+ return direction === "undo" ? inverse.before : inverse.after;
9854
+ }
9855
+ /** Composition keeps the first inverse only when every committed link belongs to the same target. */
9856
+ function composeJournalCommitReceipts(receipts, targetKey) {
9857
+ const first = receipts[0], last = receipts[receipts.length - 1];
9858
+ if (first === undefined || last === undefined) return null;
9859
+ const continuous = receipts.every((receipt, index) => {
9860
+ const previous = receipts[index - 1];
9861
+ return previous === undefined || targetKey(previous) === targetKey(receipt) && previous.revision === receipt.expectedRevision && authoredValuesEqual(previous.after, receipt.before);
9862
+ });
9863
+ return continuous ? journalCommitReceipt(last, {
9864
+ before: first.before,
9865
+ after: last.after
9866
+ }, {
9867
+ expectedRevision: first.expectedRevision,
9868
+ revision: last.revision
9869
+ }) : null;
9870
+ }
9871
+
9872
+ //#endregion
9873
+ //#region ../core/src/committed-write-proof.ts
9874
+ /** Positive atomic storage evidence survives a failed response projection. */
9875
+ const committedWriteProofSchema = z.object({
9876
+ operationId: z.string().uuid(),
9877
+ committedAt: z.string(),
9878
+ resultingRevision: z.number().int().nonnegative()
9879
+ });
9880
+
9881
+ //#endregion
9882
+ //#region ../core/src/site-deployment-mode.ts
9883
+ /** Only Riverbank-owned themes supply persisted styling facts. */
9884
+ function usesPersistedRiverbankTheme(mode) {
9885
+ return mode !== "headless_full";
9886
+ }
9887
+
9888
+ //#endregion
9889
+ //#region ../core/src/email-vocabulary.ts
9890
+ /** Shared email identities. No renderer, dashboard or private email-package dependency. */
9891
+ const EMAIL_DEFINITIONS = {
9892
+ event_confirmation: {
9893
+ wording: "editable",
9894
+ perEvent: true,
9895
+ templateStorage: true
9896
+ },
9897
+ event_reminder: {
9898
+ wording: "editable",
9899
+ perEvent: true,
9900
+ templateStorage: true
9901
+ },
9902
+ event_feedback_request: {
9903
+ wording: "editable",
9904
+ perEvent: false,
9905
+ templateStorage: true
9906
+ },
9907
+ event_cancellation_customer: {
9908
+ wording: "editable",
9909
+ perEvent: false,
9910
+ templateStorage: true
9911
+ },
9912
+ event_cancellation_admin: {
9913
+ wording: "editable",
9914
+ perEvent: false,
9915
+ templateStorage: true
9916
+ },
9917
+ event_refund_customer: {
9918
+ wording: "editable",
9919
+ perEvent: false,
9920
+ templateStorage: true
9921
+ },
9922
+ event_refund_admin: {
9923
+ wording: "editable",
9924
+ perEvent: false,
9925
+ templateStorage: true
9926
+ },
9927
+ event_transfer_customer: {
9928
+ wording: "editable",
9929
+ perEvent: false,
9930
+ templateStorage: true
9931
+ },
9932
+ event_transfer_admin: {
9933
+ wording: "editable",
9934
+ perEvent: false,
9935
+ templateStorage: true
9936
+ },
9937
+ event_booking_rescheduled_customer: {
9938
+ wording: "editable",
9939
+ perEvent: false,
9940
+ templateStorage: true
9941
+ },
9942
+ event_booking_moved_admin: {
9943
+ wording: "editable",
9944
+ perEvent: false,
9945
+ templateStorage: true
9946
+ },
9947
+ pass_purchase_confirmation: {
9948
+ wording: "editable",
9949
+ perEvent: false,
9950
+ templateStorage: true
9951
+ },
9952
+ membership_purchase_confirmation: {
9953
+ wording: "editable",
9954
+ perEvent: false,
9955
+ templateStorage: true
9956
+ },
9957
+ membership_cancel_at_period_end: {
9958
+ wording: "editable",
9959
+ perEvent: false,
9960
+ templateStorage: true
9961
+ },
9962
+ membership_payment_refund: {
9963
+ wording: "editable",
9964
+ perEvent: false,
9965
+ templateStorage: true
9966
+ },
9967
+ appointment_confirmation: {
9968
+ wording: "editable",
9969
+ perEvent: false,
9970
+ templateStorage: true
9971
+ },
9972
+ appointment_reminder: {
9973
+ wording: "editable",
9974
+ perEvent: false,
9975
+ templateStorage: true
9976
+ },
9977
+ appointment_follow_up: {
9978
+ wording: "editable",
9979
+ perEvent: false,
9980
+ templateStorage: true
9981
+ },
9982
+ site_credit_adjustment: {
9983
+ wording: "editable",
9984
+ perEvent: false,
9985
+ templateStorage: true
9986
+ },
9987
+ site_credit_refund_conversion: {
9988
+ wording: "editable",
9989
+ perEvent: false,
9990
+ templateStorage: true
9991
+ },
9992
+ order_refund: {
9993
+ wording: "editable",
9994
+ perEvent: false,
9995
+ templateStorage: true
9996
+ },
9997
+ pass_refund: {
9998
+ wording: "editable",
9999
+ perEvent: false,
10000
+ templateStorage: true
10001
+ },
10002
+ event_series_registration_confirmation: {
10003
+ wording: "editable",
10004
+ perEvent: false,
10005
+ templateStorage: true
10006
+ },
10007
+ event_series_registration_cancellation_customer: {
10008
+ wording: "editable",
10009
+ perEvent: false,
10010
+ templateStorage: true
10011
+ },
10012
+ event_series_registration_cancellation_admin: {
10013
+ wording: "editable",
10014
+ perEvent: false,
10015
+ templateStorage: true
10016
+ },
10017
+ event_series_registration_refund_customer: {
10018
+ wording: "editable",
10019
+ perEvent: false,
10020
+ templateStorage: true
10021
+ },
10022
+ event_series_registration_refund_admin: {
10023
+ wording: "editable",
10024
+ perEvent: false,
10025
+ templateStorage: true
10026
+ },
10027
+ event_update: {
10028
+ wording: "editable",
10029
+ perEvent: false,
10030
+ templateStorage: true
10031
+ },
10032
+ event_cancelled_by_organizer: {
10033
+ wording: "editable",
10034
+ perEvent: true,
10035
+ templateStorage: true
10036
+ },
10037
+ digital_product_fulfillment: {
10038
+ wording: "editable",
10039
+ perEvent: false,
10040
+ templateStorage: true
10041
+ },
10042
+ order_confirmation: {
10043
+ wording: "editable",
10044
+ perEvent: false,
10045
+ templateStorage: true
10046
+ },
10047
+ order_shipment: {
10048
+ wording: "editable",
10049
+ perEvent: false,
10050
+ templateStorage: true
10051
+ },
10052
+ order_cancellation: {
10053
+ wording: "editable",
10054
+ perEvent: false,
10055
+ templateStorage: true
10056
+ },
10057
+ order_payment_reminder_customer: {
10058
+ wording: "editable",
10059
+ perEvent: false,
10060
+ templateStorage: true
10061
+ },
10062
+ order_balance_due_customer: {
10063
+ wording: "editable",
10064
+ perEvent: false,
10065
+ templateStorage: true
10066
+ },
10067
+ order_balance_overdue_customer: {
10068
+ wording: "editable",
10069
+ perEvent: false,
10070
+ templateStorage: true
10071
+ },
10072
+ order_balance_overdue_admin: {
10073
+ wording: "editable",
10074
+ perEvent: false,
10075
+ templateStorage: true
10076
+ },
10077
+ form_submitter_auto_reply: {
10078
+ wording: "editable",
10079
+ perEvent: false,
10080
+ templateStorage: true
10081
+ },
10082
+ appointment_request_received: {
10083
+ wording: "editable",
10084
+ perEvent: false,
10085
+ templateStorage: true
10086
+ },
10087
+ appointment_request_declined: {
10088
+ wording: "editable",
10089
+ perEvent: false,
10090
+ templateStorage: true
10091
+ },
10092
+ booking_questions_reminder: {
10093
+ wording: "editable",
10094
+ perEvent: false,
10095
+ templateStorage: true
10096
+ },
10097
+ bundle_booking_confirmation: {
10098
+ wording: "shared",
10099
+ sharesWordingWith: "event_confirmation",
10100
+ perEvent: false,
10101
+ templateStorage: true
10102
+ },
10103
+ appointment_rescheduled_customer: {
10104
+ wording: "editable",
10105
+ perEvent: false,
10106
+ templateStorage: true
10107
+ },
10108
+ appointment_rescheduled_team: {
10109
+ wording: "fixed",
10110
+ perEvent: false,
10111
+ templateStorage: false
10112
+ },
10113
+ event_venue_changed: {
10114
+ wording: "editable",
10115
+ perEvent: false,
10116
+ templateStorage: true
10117
+ },
10118
+ course_session_added: {
10119
+ wording: "editable",
10120
+ perEvent: false,
10121
+ templateStorage: true
10122
+ },
10123
+ appointment_cancellation: {
10124
+ wording: "editable",
10125
+ perEvent: false,
10126
+ templateStorage: true
10127
+ },
10128
+ course_session_dropped: {
10129
+ wording: "editable",
10130
+ perEvent: false,
10131
+ templateStorage: true
10132
+ },
10133
+ event_series_waitlist_joined: {
10134
+ wording: "editable",
10135
+ perEvent: false,
10136
+ templateStorage: true
10137
+ },
10138
+ event_waitlist_join_confirmation: {
10139
+ wording: "editable",
10140
+ perEvent: false,
10141
+ templateStorage: true
10142
+ },
10143
+ waitlist_claim: {
10144
+ wording: "editable",
10145
+ perEvent: false,
10146
+ templateStorage: true
10147
+ },
10148
+ gift_card_delivery: {
10149
+ wording: "editable",
10150
+ perEvent: false,
10151
+ templateStorage: true
10152
+ },
10153
+ voucher_delivery: {
10154
+ wording: "editable",
10155
+ perEvent: false,
10156
+ templateStorage: true
10157
+ },
10158
+ event_team_new_booking: {
10159
+ wording: "fixed",
10160
+ perEvent: false,
10161
+ templateStorage: false
10162
+ },
10163
+ event_team_cancellation: {
10164
+ wording: "fixed",
10165
+ perEvent: false,
10166
+ templateStorage: false
10167
+ },
10168
+ event_team_transfer: {
10169
+ wording: "fixed",
10170
+ perEvent: false,
10171
+ templateStorage: false
10172
+ },
10173
+ course_team_new_booking: {
10174
+ wording: "fixed",
10175
+ perEvent: false,
10176
+ templateStorage: false
10177
+ },
10178
+ course_team_cancellation: {
10179
+ wording: "fixed",
10180
+ perEvent: false,
10181
+ templateStorage: false
10182
+ },
10183
+ appointment_team_new_booking: {
10184
+ wording: "fixed",
10185
+ perEvent: false,
10186
+ templateStorage: false
10187
+ },
10188
+ appointment_team_new_request: {
10189
+ wording: "fixed",
10190
+ perEvent: false,
10191
+ templateStorage: false
10192
+ },
10193
+ appointment_team_cancellation: {
10194
+ wording: "fixed",
10195
+ perEvent: false,
10196
+ templateStorage: false
10197
+ },
10198
+ appointment_team_staff_change: {
10199
+ wording: "fixed",
10200
+ perEvent: false,
10201
+ templateStorage: false
10202
+ },
10203
+ appointment_staff_daily_digest: {
10204
+ wording: "fixed",
10205
+ perEvent: false,
10206
+ templateStorage: false
10207
+ },
10208
+ form_owner_notification: {
10209
+ wording: "fixed",
10210
+ perEvent: false,
10211
+ templateStorage: false
10212
+ },
10213
+ support_ticket_notification: {
10214
+ wording: "fixed",
10215
+ perEvent: false,
10216
+ templateStorage: false
10217
+ },
10218
+ support_ticket_customer_reply: {
10219
+ wording: "fixed",
10220
+ perEvent: false,
10221
+ templateStorage: false
10222
+ },
10223
+ support_ticket_reply: {
10224
+ wording: "fixed",
10225
+ perEvent: false,
10226
+ templateStorage: false
10227
+ },
10228
+ auth_confirmation: {
10229
+ wording: "fixed",
10230
+ perEvent: false,
10231
+ templateStorage: false
10232
+ },
10233
+ password_reset: {
10234
+ wording: "fixed",
10235
+ perEvent: false,
10236
+ templateStorage: false
10237
+ },
10238
+ portal_magic_link: {
10239
+ wording: "fixed",
10240
+ perEvent: false,
10241
+ templateStorage: true
10242
+ },
10243
+ site_invitation: {
10244
+ wording: "fixed",
10245
+ perEvent: false,
10246
+ templateStorage: false
10247
+ },
10248
+ demo_claim_preparation_failed: {
10249
+ wording: "fixed",
10250
+ perEvent: false,
10251
+ templateStorage: false
10252
+ },
10253
+ billing_payment_issue: {
10254
+ wording: "fixed",
10255
+ perEvent: false,
10256
+ templateStorage: false
10257
+ },
10258
+ order_instalment_payment_failed: {
10259
+ wording: "fixed",
10260
+ perEvent: false,
10261
+ templateStorage: true
10262
+ },
10263
+ macadamia_contact_inquiry: {
10264
+ wording: "fixed",
10265
+ perEvent: false,
10266
+ templateStorage: false
10267
+ },
10268
+ newsletter_confirmation: {
10269
+ wording: "fixed",
10270
+ perEvent: false,
10271
+ templateStorage: true
10272
+ },
10273
+ newsletter_campaign_test_send: {
10274
+ wording: "written_each_time",
10275
+ perEvent: false,
10276
+ templateStorage: false
10277
+ },
10278
+ newsletter_campaign_delivery: {
10279
+ wording: "written_each_time",
10280
+ perEvent: false,
10281
+ templateStorage: false
10282
+ },
10283
+ site_email_template_test_send: {
10284
+ wording: "written_each_time",
10285
+ perEvent: false,
10286
+ templateStorage: false
10287
+ },
10288
+ participant_message: {
10289
+ wording: "written_each_time",
10290
+ perEvent: false,
10291
+ templateStorage: false
10292
+ }
10293
+ };
10294
+ const EMAIL_KEYS = Object.keys(EMAIL_DEFINITIONS);
10295
+ const EMAIL_TEMPLATE_TYPES = EMAIL_KEYS.filter((key) => EMAIL_DEFINITIONS[key].wording === "editable");
10296
+ const EVENT_EMAIL_TEMPLATE_TYPES = EMAIL_TEMPLATE_TYPES.filter((key) => EMAIL_DEFINITIONS[key].perEvent);
10297
+ /** Includes upcoming wording conversions; storage admission doesn't expose an editor. */
10298
+ const EMAIL_TEMPLATE_STORAGE_KEYS = EMAIL_KEYS.filter((key) => EMAIL_DEFINITIONS[key].templateStorage);
10299
+ function isEmailKey(value) {
10300
+ return Object.prototype.hasOwnProperty.call(EMAIL_DEFINITIONS, value);
10301
+ }
10302
+ function isEventEmailTemplateType(value) {
10303
+ return EVENT_EMAIL_TEMPLATE_TYPES.some((key) => key === value);
10304
+ }
10305
+
10306
+ //#endregion
10307
+ export { mapDrawingSchema as $, ok as $n, isNonNegativeWholeCents as $t, assessMailingListTargeting as A, unsafeAsEntitlementId as An, addIntervalPreservingMonthAnchor as At, parsePublicContactDetails as B, unsafeAsPositiveSeconds as Bn, parseEventPageSurfaceSubject as Bt, exposeSecret as C, unsafeAsAppointmentResourceId as Cn, toScheduledBookingDateTime as Ct, parseMailingListContentVersionId as D, unsafeAsDeclaredMimeType as Dn, isLiveOrderInstalmentPlanStatus as Dt, parseMailingListBlockId as E, unsafeAsCheckoutLineId as En, validateBookingPaymentOptionKinds as Et, parseMailingListPlacementTargeting as F, unsafeAsMediaAssetId as Fn, omitZeroAmountFutureRows as Ft, locationMapLinks as G, unsafeAsSiteId as Gn, publicEventPricingDisplay as Gt, assertAllowedPublicUrl as H, unsafeAsProductId as Hn, formatEventPricing as Ht, DEFAULT_MAILING_LIST_TARGETING as I, unsafeAsOccurrenceId as In, splitAmountWithRemainderInFirst as It, parseLocationValue as J, unsafeAsStorageBucketName as Jn, resolveExactOrFreePriceDisplay as Jt, locationValueSchema as K, unsafeAsSiteScopedStoragePath as Kn, publicEventPricingOptions as Kt, planMailingListChoicePresentation as L, unsafeAsOriginalFilename as Ln, resolveFlexibleBalanceDueAt as Lt, parseMailingListProviderScope as M, unsafeAsEventOfferingId as Mn, getBookingInstalmentDepositRefundability as Mt, parseFooterCompositionV1 as N, unsafeAsEventSeriesId as Nn, getBookingInstalmentPlanIntervalCount as Nt, parseMailingListSignedSourceToken as O, unsafeAsDemoCapability as On, isSettleableFlexibleBalanceContractStatus as Ot, footerNavigationLayouts as P, unsafeAsFileChecksumSha256 as Pn, getBookingInstalmentProtectedDepositAmountCents as Pt, isPresent as Q, err as Qn, mapReadonlyNonEmptyArray as Qt, isCanonicalRichTextDoc as R, unsafeAsPassId as Rn, resolveFlexibleBalanceInitialPaymentCents as Rt, createSecret as S, parseUserId as Sn, toBookingTimeZone as St, slugify as T, unsafeAsBookingSessionId as Tn, parseBookingTimeZone as Tt, decideLocationSave as U, unsafeAsSafeDownloadFilename as Un, isPublicEventPricingOptionDisplayable as Ut, parseWeeklyOpeningHours as V, unsafeAsPricingOptionId as Vn, formatBookingPriceDisplay as Vt, stableJsonStringify as W, unsafeAsSiteFontId as Wn, publicEventPricingCurrency as Wt, publicLocationSchema as X, unsafeAsUploadSessionId as Xn, fromArray as Xt, projectPublicLocation as Y, unsafeAsStoragePath as Yn, resolvePricingOptionsPriceDisplay as Yt, toLocationCoordinates as Z, unsafeAsUserId as Zn, mapNonEmptyArray as Zt, CANCELLED_SESSION_STATUS as _, parseMembershipId as _n, parseBookingLocalDate as _t, NAVIGATION_LOCATIONS as a, unsafeAsPositiveCents as an, formatTime as at, resolveWholeSeriesBookingPhase as b, parseSiteId as bn, resolveBookingBusinessTimeZone as bt, parseVerticalId as c, assertNever as cn, resolveBookingDisplayContext as ct, DEFAULT_EVENT_TYPE_PRESETS as d, parseCustomerId as dn, formatLocalScheduleDate as dt, parseNonNegativeCents as en, selectCategoryView as et, EVENT_PRESET_VALUES as f, parseDatedOfferingCapacityHoldAuditRef as fn, formatLocalScheduleTime as ft, publicReadFreshnessSeconds as g, parseMediaAssetId as gn, localScheduleTimePattern as gt, WHOLE_SERIES_EVENT_PRESET_VALUES as h, parseEventSeriesId as hn, getLocalScheduleTime as ht, EVENT_PRESET_VOCABULARY as i, unsafeAsNonNegativeCents as in, formatDateTimeRange as it, DEFAULT_MAILING_LIST_SUCCESS_MESSAGE as j, unsafeAsEventInstanceId as jn, clampConfiguredAmount as jt, parseMailingListRuntimeContext as k, unsafeAsDetectedMimeType as kn, isSettleableOrderInstalmentPlanStatus as kt, parseVerticalPresetId as l, formatDurationUnitLabel as ln, buildBookingUtcDateWindow as lt, SURFACE_EVENT_PRESET_VALUES as m, parseEditorSessionGeneration as mn, getLocalScheduleDate as mt, authoredValuesEqual as n, toCents as nn, formatDateRange as nt, areConfiguredBookingVerticalConfigsEqual as o, formatCurrencyAmount as on, formatTimeRange as ot, ORDINARY_EVENT_PRESET_VALUES as p, parseDatedOfferingCapacityHoldId as pn, getBookingLocalDateKey as pt, normalizeLocationAddressText as q, unsafeAsStaffMemberId as qn, publicEventPricingType as qt, resolveEventCatalogue as r, toNonNegativeCents as rn, formatDateTime as rt, canonicalizeConfiguredBookingVerticalConfig as s, parseCurrencyCode as sn, inferSiteDateLocale as st, committedWriteProofSchema as t, parsePositiveCents as tn, formatDate as tt, resolveSiteVerticalBookingConfig as u, parseApiKeyId as un, compareLocalScheduleDates as ut, nextLiveRunSessionStartMs as v, parsePageId as vn, parseLocalScheduleTime as vt, parseManagementKey as w, unsafeAsBookingRequirementId as wn, toUtcIsoDateTimeString as wt, resolveWholeSeriesRunLifecycle as x, parseSiteTermsVersionId as xn, resolveBookingTimeZoneId as xt, orderedLiveRunSessions as y, parseRetiredCourseId as yn, parseUtcIsoDateTimeString as yt, formatOpeningHoursRows as z, unsafeAsPositiveByteCount as zn, deriveTotalTicketQuantity as zt };