@v-office/website-headless 0.2.0 → 1.1.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.
@@ -0,0 +1,2724 @@
1
+ import { FilterGetFiltersOutput, Locale, RentalRentalsOutput, SearchSearchSort, WebsiteSDKConfig, WebsiteSDKOptions, createWebsiteSDK } from "@v-office/website-sdk";
2
+ import { BackendKind } from "@v-office/website-sdk/capabilities/search";
3
+ //#region src/enhance/parts.d.ts
4
+ /** Every `data-part` value an engine binds, by role (the value object is declared below):
5
+ * - strips (`bindStrip`): `strip` root, `strip-track` (the horizontal scroller), `strip-item`,
6
+ * `strip-prev` / `strip-next`, `strip-dots` (one child per item), `strip-counter`;
7
+ * - layers (`bindLayer`): `layer` (a `<dialog>`), `layer-open` (with `aria-controls` = the dialog id),
8
+ * `layer-close`, `layer-body` (the inner scroller reset on close);
9
+ * - reveal and clamp (`bindReveal`, `bindClamp`): `reveal`, `reveal-item`, `reveal-more`, `reveal-less`;
10
+ * `clamp`, `clamp-text`, `clamp-toggle`;
11
+ * - share and favorites (`bindShare`, `bindFavorites`): `share`, `share-status`, `favorite`, `favorite-badge`;
12
+ * - the rental gallery (`bindGallery`): `rental-gallery` root, `rental-gallery-tile`, `rental-gallery-all`,
13
+ * the `<dialog>`s `rental-gallery-grid`, `rental-gallery-viewer`, `rental-floorplan`, the per-photo
14
+ * `rental-gallery-caption` / `rental-gallery-counter`, and `rental-floorplan-open`. */
15
+ type EnhanceParts = typeof ENHANCE_PARTS;
16
+ /** One engine part name. */
17
+ type EnhancePart = EnhanceParts[keyof EnhanceParts];
18
+ /** Attributes the blueprints gates read today (fact-check, room measurements). The rental blocks render
19
+ * them beside their parts, on the same elements as the kits did, until the gates read parts. */
20
+ type GateMarkers = typeof GATE_MARKERS;
21
+ declare const ENHANCE_PARTS: {
22
+ readonly strip: "strip";
23
+ readonly stripTrack: "strip-track";
24
+ readonly stripItem: "strip-item";
25
+ readonly stripPrev: "strip-prev";
26
+ readonly stripNext: "strip-next";
27
+ readonly stripDots: "strip-dots";
28
+ readonly stripCounter: "strip-counter";
29
+ readonly layer: "layer";
30
+ readonly layerOpen: "layer-open";
31
+ readonly layerClose: "layer-close";
32
+ readonly layerBody: "layer-body";
33
+ readonly reveal: "reveal";
34
+ readonly revealItem: "reveal-item";
35
+ readonly revealMore: "reveal-more";
36
+ readonly revealLess: "reveal-less";
37
+ readonly clamp: "clamp";
38
+ readonly clampText: "clamp-text";
39
+ readonly clampToggle: "clamp-toggle";
40
+ readonly share: "share";
41
+ readonly shareStatus: "share-status";
42
+ readonly favorite: "favorite";
43
+ readonly favoriteBadge: "favorite-badge";
44
+ readonly gallery: "rental-gallery";
45
+ readonly galleryTile: "rental-gallery-tile";
46
+ readonly galleryAll: "rental-gallery-all";
47
+ readonly galleryGrid: "rental-gallery-grid";
48
+ readonly galleryViewer: "rental-gallery-viewer";
49
+ readonly galleryCaption: "rental-gallery-caption";
50
+ readonly galleryCounter: "rental-gallery-counter";
51
+ readonly floorplanOpen: "rental-floorplan-open";
52
+ readonly floorplan: "rental-floorplan";
53
+ };
54
+ declare const GATE_MARKERS: {
55
+ readonly stats: "data-rental-stats";
56
+ readonly descriptionStart: "data-rental-description";
57
+ readonly descriptionEnd: "data-rental-description-end";
58
+ readonly amenities: "data-rental-amenities";
59
+ readonly amenitiesModal: "data-amenities-modal";
60
+ readonly amenitiesEnd: "data-amenities-overlay-end";
61
+ readonly roomAttribute: "data-room-attr";
62
+ readonly reviewCard: "data-review-card";
63
+ };
64
+ //#endregion
65
+ //#region src/shared/rich-text.d.ts
66
+ /** Inline content. Text is plain (the renderer escapes it). */
67
+ type RichInline =
68
+ /** A run of text with its emphasis. */
69
+ {
70
+ readonly type: "text";
71
+ readonly text: string;
72
+ readonly strong?: true;
73
+ readonly em?: true;
74
+ readonly code?: true;
75
+ } |
76
+ /** A line break inside a paragraph. */
77
+ {
78
+ readonly type: "break";
79
+ } |
80
+ /** href already whitelisted (https, http, mailto, tel, or a site-relative `/path` or `#anchor`); any
81
+ * other target became text. `external` = an http(s) URL (the renderer opens it in a new tab). */
82
+ {
83
+ readonly type: "link";
84
+ readonly href: string;
85
+ readonly external: boolean;
86
+ readonly children: readonly RichInline[];
87
+ };
88
+ /** Block content. Heading levels are the document's own, clamped to 2–4; the renderer offsets them. */
89
+ type RichBlock =
90
+ /** A heading with text (empty headings are dropped). */
91
+ {
92
+ readonly type: "heading";
93
+ readonly level: 2 | 3 | 4;
94
+ readonly children: readonly RichInline[];
95
+ } |
96
+ /** A paragraph with text. */
97
+ {
98
+ readonly type: "paragraph";
99
+ readonly children: readonly RichInline[];
100
+ } |
101
+ /** A list; each item is its own block flow. */
102
+ {
103
+ readonly type: "list";
104
+ readonly ordered: boolean;
105
+ readonly items: readonly (readonly RichBlock[])[];
106
+ } |
107
+ /** A horizontal rule. */
108
+ {
109
+ readonly type: "rule";
110
+ };
111
+ /** How `parseTextBlocks` reads PMS plain text. "light": `**bold**` → strong, `- `/`* ` lines → a list,
112
+ * so no asterisk reaches a guest (a stray `**` is dropped). "plain": the text verbatim. */
113
+ type TextMarkup = "light" | "plain";
114
+ /** Options of `normalizeStructuredDocument`. */
115
+ interface StructuredDocumentOptions {
116
+ /** The document's subject: a leading heading repeating it is removed (the page draws its own). */
117
+ readonly title?: string;
118
+ }
119
+ /** Options of `plainText`. */
120
+ interface PlainTextOptions {
121
+ /** Cut at a word boundary so the result, " …" included, is at most this long. Default: no cut. */
122
+ readonly max?: number;
123
+ }
124
+ /** PMS plain text → blocks: blank lines separate paragraphs, single newlines collapse to a space. */
125
+ type ParseTextBlocks = (text: string, markup?: TextMarkup) => RichBlock[];
126
+ /** The SDK's structured-document nodes (`static.documents`) → blocks: editor markers and context-bound
127
+ * tools (`ai`, `conditional`, `snippet`, `image`, `quote`) dropped, unknown tools keep their children,
128
+ * inline runs grouped into paragraphs ("\n" = break, a blank line = new paragraph), a leading heading
129
+ * repeating `title` removed, empty headings dropped, non-whitelisted hrefs become text. */
130
+ type NormalizeStructuredDocument = (nodes: readonly unknown[], options?: StructuredDocumentOptions) => RichBlock[];
131
+ /** Plain text of blocks (markup removed, whitespace collapsed), for a meta description or an alt text. */
132
+ type PlainText = (blocks: readonly RichBlock[], options?: PlainTextOptions) => string;
133
+ /** See {@link ParseTextBlocks}. */
134
+ declare const parseTextBlocks: ParseTextBlocks;
135
+ /** See {@link NormalizeStructuredDocument}. */
136
+ declare const normalizeStructuredDocument: NormalizeStructuredDocument;
137
+ /** See {@link PlainText}. */
138
+ declare const plainText: PlainText;
139
+ //#endregion
140
+ //#region src/rental/model.d.ts
141
+ /** One item of `static.rentals.getRentals` — the adapter's only input. */
142
+ type CatalogueRental = RentalRentalsOutput;
143
+ /** A rental as the detail page renders it. */
144
+ interface RentalDetail {
145
+ /** PMS rental id, verbatim. */
146
+ id: string;
147
+ /** URL slug from `assignSlugs`; search results link with the same map. */
148
+ slug: string;
149
+ /** `nameOrLabel`, verbatim (v9 property units: property + unit). */
150
+ name: string;
151
+ /** `address.city`, or undefined — no fallback town (a fallback prints a wrong place). */
152
+ town?: string;
153
+ /** The address parts the PMS states (printing the street is the site's decision). */
154
+ address: RentalAddress;
155
+ /** The coordinates the PMS publishes. */
156
+ coords?: {
157
+ lat: number;
158
+ lng: number;
159
+ };
160
+ /** The SDK-translated rental type ("Ferienwohnung"), verbatim; often absent on v9. */
161
+ type?: string;
162
+ /** Counts the feed states (`parseCounts`). */
163
+ counts: RentalCounts;
164
+ /** Joined by `fetchRentalFlags`; a flag the backend could not answer is absent. */
165
+ flags: Partial<Record<RentalFlag, boolean>>;
166
+ /** Distances the vicinity states, one per kind, in feed order. */
167
+ distances: RentalDistance[];
168
+ /** Photos in feed order: duplicates dropped, floor plans excluded, WebP where served. */
169
+ images: RentalImage[];
170
+ /** Images tagged `category: "floorplan"`, as an uncropped rendition, in feed order. */
171
+ floorplans: RentalImage[];
172
+ /** The PMS description: headline, intro, body and (when published) the arrival info. */
173
+ description: DescriptionContent;
174
+ /** SDK highlights (its default prioritisation), reordered by `rankHighlights`, never
175
+ * dropped. Count echoes stay; consumers skip them with `isSpecEcho`. */
176
+ highlights: string[];
177
+ /** The attribute catalogue by PMS category; unsafe categories and prose items removed. */
178
+ amenities: AmenityGroup[];
179
+ /** Vicinity "Themen" / "Themes" items, verbatim. */
180
+ themes: string[];
181
+ /** Every vicinity category with the items that are not distances ("Lage": "Am Strand",
182
+ * "Meerblick"), verbatim — what `{ stated }` card matching reads besides the amenities. */
183
+ vicinity: AmenityGroup[];
184
+ /** Every room the feed states, in SDK order. */
185
+ rooms: RentalRoom[];
186
+ /** The rating summary and the reviews with text. */
187
+ reviews: RentalReviews;
188
+ /** The property (Anlage) the rental belongs to, when the feed states one. */
189
+ property?: RentalPropertyInfo;
190
+ }
191
+ /** The address parts the PMS states; whether a page prints the street is the site's decision. */
192
+ interface RentalAddress {
193
+ /** Street and number, verbatim. */
194
+ street?: string;
195
+ /** Postal code, verbatim. */
196
+ postalcode?: string;
197
+ /** Town, verbatim. */
198
+ city?: string;
199
+ /** Region or state, verbatim. */
200
+ province?: string;
201
+ /** Country, verbatim. */
202
+ country?: string;
203
+ }
204
+ /** Booleans `getRentals` lacks, answered by dateless live searches (`fetchRentalFlags`). */
205
+ type RentalFlag = "petsAllowed" | "atBeach" | "inCity" | "seaView" | "lakeView" | "garage" | "indoorPool" | "garden";
206
+ /** One photo of a rental or its property. */
207
+ interface RentalImage {
208
+ /** SDK `idOrPath`; stable, usable as a key. */
209
+ id: string;
210
+ /** The image URL (WebP where served). */
211
+ src: string;
212
+ /** Responsive candidates, when the feed offers renditions. */
213
+ srcset?: string;
214
+ /** The PMS caption (`alt`, else `title`) when it says more than the rental's name. */
215
+ caption?: string;
216
+ /** Intrinsic size when the feed states it (`metadata.dataImage`). */
217
+ width?: number;
218
+ /** Intrinsic height when the feed states it. */
219
+ height?: number;
220
+ /** The rental's own photo, or one of its property's (`AdaptRentalOptions.propertyImages`). */
221
+ source: "rental" | "property";
222
+ }
223
+ /** The PMS description texts. */
224
+ interface DescriptionContent {
225
+ /** PMS `description.headline`. */
226
+ headline?: string;
227
+ /** PMS `description.intro` (the short description). */
228
+ intro?: string;
229
+ /** PMS `description.description` as blocks (`intro` when the body is empty). */
230
+ body: RichBlock[];
231
+ /** PMS `description.arrivalInfo`, present only with `publishArrivalInfo: true`. */
232
+ arrivalInfo?: RichBlock[];
233
+ }
234
+ /** One PMS amenity or vicinity category with its items. */
235
+ interface AmenityGroup {
236
+ /** The PMS category label ("Küche"). */
237
+ label: string;
238
+ /** Items verbatim, in SDK order ("1977 Baujahr" stays). */
239
+ items: string[];
240
+ }
241
+ /** What a room is, for its glyph. Unknown types are "other" — never a bedroom. */
242
+ type RoomKind = "bedroom" | "bathroom" | "living" | "kitchen" | "other";
243
+ /** One room the feed states. */
244
+ interface RentalRoom {
245
+ /** PMS room type, verbatim ("Schlafzimmer", "Sonstiges"). */
246
+ type: string;
247
+ /** What the room is, for its glyph. */
248
+ kind: RoomKind;
249
+ /** 1-based position among rooms of the same type, set only when the type repeats. */
250
+ ordinal?: number;
251
+ /** PMS room description, when stated. */
252
+ description?: string;
253
+ /** Beds as runs: consecutive identical beds collapse into a count, never de-duplicated. */
254
+ beds: BedRun[];
255
+ /** Room attributes verbatim, the floor-covering family (and `hideRoomAttributes`) removed. */
256
+ attributes: string[];
257
+ }
258
+ /** Consecutive identical beds of a room. */
259
+ interface BedRun {
260
+ /** PMS bed type ("Standard Bett"); may be "" after an SDK translation override. */
261
+ type: string;
262
+ /** PMS bed kind ("Einzelbett"). */
263
+ kind?: string;
264
+ /** Consecutive identical beds in this run (the SDK repeats an entry per bed). */
265
+ count: number;
266
+ /** Bed attributes verbatim. */
267
+ attributes: string[];
268
+ }
269
+ /** A rental's guest reviews. */
270
+ interface RentalReviews {
271
+ /** Absent when the PMS holds no aggregate; every review UI then self-hides. */
272
+ summary?: ReviewSummary;
273
+ /** Reviews with text, newest first (`newestReviewsFirst`). */
274
+ items: RentalReview[];
275
+ }
276
+ /** The PMS's rating aggregate. */
277
+ interface ReviewSummary {
278
+ /** "4.3/5" → 4.3. */
279
+ score: number;
280
+ /** "4.3/5" → 5 (5 when the feed states no scale). */
281
+ scale: number;
282
+ /** "4 Bewertungen" → 4. */
283
+ count: number;
284
+ /** The PMS's own verbal grade (its "classification"), verbatim. No grade is ever derived from the
285
+ * score. */
286
+ grade?: string;
287
+ /** Per-criterion means, PMS labels verbatim (v9: the site's `v9Reviews.criteria` labels). */
288
+ criteria: ReviewCriterion[];
289
+ }
290
+ /** One rated criterion's mean. */
291
+ interface ReviewCriterion {
292
+ /** The criterion, PMS label verbatim ("Sauberkeit der Unterkunft"). */
293
+ label: string;
294
+ /** Its mean on the summary's scale. */
295
+ value: number;
296
+ }
297
+ /** One guest review with text. */
298
+ interface RentalReview {
299
+ /** The PMS review id, when stated (a stable key). */
300
+ id?: string;
301
+ /** The review's own score on the summary's scale, when stated. */
302
+ score?: number;
303
+ /** The review text, verbatim. */
304
+ text: string;
305
+ /** PMS review label (a title), when stated. */
306
+ title?: string;
307
+ /** "Max M."; undefined when the feed states no name or a placeholder (the UI says "Gast"). */
308
+ reviewer?: string;
309
+ /** The stay as the SDK wrote it after the name ("verreist im Mai 2025"), verbatim. */
310
+ stay?: string;
311
+ }
312
+ /** The property (Anlage) a rental belongs to. */
313
+ interface RentalPropertyInfo {
314
+ /** PMS property id. */
315
+ id: string;
316
+ /** The property's name. */
317
+ name: string;
318
+ /** Its description as blocks. */
319
+ description: RichBlock[];
320
+ /** Its highlights, verbatim. */
321
+ highlights: string[];
322
+ /** Its photos. */
323
+ images: RentalImage[];
324
+ }
325
+ //#endregion
326
+ //#region src/rental/facts.d.ts
327
+ /** Counts the feed states. Undefined = the feed states nothing — never print a 0. */
328
+ interface RentalCounts {
329
+ /** Most guests. */
330
+ guests?: number;
331
+ /** Bedrooms. */
332
+ bedrooms?: number;
333
+ /** Bathrooms. */
334
+ bathrooms?: number;
335
+ /** Total rooms ("3 Zimmer"). */
336
+ rooms?: number;
337
+ /** Living area in m². */
338
+ area?: number;
339
+ }
340
+ /** What a distance is measured to. Locale-free: the UI's copy names it. */
341
+ type DistanceKind = "beach" | "sea" | "lake" | "city" | "ski" | "golf" | "trainStation" | "ferry" | "airport" | "shopping" | "restaurant" | "publicTransport";
342
+ /** One distance the vicinity states, in metres. Never estimated. */
343
+ interface RentalDistance {
344
+ /** What the distance is measured to. */
345
+ kind: DistanceKind;
346
+ /** The distance in metres, as stated. */
347
+ meters: number;
348
+ /** The PMS label as stated ("Entfernung zum Strand"), for a site printing its own wording. */
349
+ label: string;
350
+ }
351
+ /** Below this a distance is a statement ("Direkt am Strand"), not a number. 25, not 0 or 50:
352
+ * measured on the fleet's feeds (0 was never the real boundary). */
353
+ declare const ADJACENT_BELOW_M: 25;
354
+ /** The "water" fact's kinds by priority — Strand > Meer > See, never city (not water). */
355
+ declare const WATER_KINDS: readonly DistanceKind[];
356
+ /** Counts from `roomSummary` first, then `highlights` (v9 states no `roomSummary`, so its
357
+ * bathrooms come from the highlights). */
358
+ type ParseCounts = (sources: {
359
+ readonly roomSummary?: readonly string[] | null | undefined;
360
+ readonly highlights?: readonly string[] | null | undefined;
361
+ }) => RentalCounts;
362
+ /** Distances the vicinity states, one per kind (the first stated wins), in feed order. Unknown
363
+ * labels are ignored, never guessed. */
364
+ type ParseDistances = (vicinity: CatalogueRental["vicinity"]) => RentalDistance[];
365
+ /** The first stated distance of the first kind in `kinds` (default WATER_KINDS) — by priority,
366
+ * never the nearest: nearest flips between lake and sea per rental and is not additive. */
367
+ type DistanceFact = (distances: readonly RentalDistance[] | undefined, kinds?: readonly DistanceKind[]) => RentalDistance | undefined;
368
+ /** True below `ADJACENT_BELOW_M`. */
369
+ type IsAdjacent = (distance: RentalDistance) => boolean;
370
+ /** A highlight that only restates a count ("4 Schlafmöglichkeiten", "61 m²"), by shape — never
371
+ * by position: how many count lines lead the list differs per rental. */
372
+ type IsSpecEcho = (label: string) => boolean;
373
+ /** See {@link ParseCounts}. */
374
+ declare const parseCounts: ParseCounts;
375
+ /** See {@link ParseDistances}. */
376
+ declare const parseDistances: ParseDistances;
377
+ /** See {@link DistanceFact}. */
378
+ declare const distanceFact: DistanceFact;
379
+ /** See {@link IsAdjacent}. */
380
+ declare const isAdjacent: IsAdjacent;
381
+ /** See {@link IsSpecEcho}. */
382
+ declare const isSpecEcho: IsSpecEcho;
383
+ //#endregion
384
+ //#region src/sdk/types.d.ts
385
+ /** A constructed website SDK (either backend). An instance built in `SdkMode` "all-rentals-at-once"
386
+ * travels under this type too; its search takes no cursor and its `mapSearch` rejects. */
387
+ type WebsiteSdk = ReturnType<typeof createWebsiteSDK>;
388
+ /** PMS generation (the SDK capability helpers' `BackendKind`). */
389
+ type Backend = "v9" | "v10";
390
+ /** Deployment stage; selects the platform's public endpoint set. */
391
+ type Stage = "development" | "production";
392
+ /** Every stage, in a stable order. */
393
+ declare const STAGES: readonly Stage[];
394
+ /**
395
+ * The SDK config as plain, public, JSON-serializable values. It IS the SDK's own `WebsiteSDKConfig`
396
+ * with `backend` explicit on v10, so every SDK field reaches the SDK — v9's optional
397
+ * `facilityObjectGroupRelationAttributeId` and `rentalDataAttributes` included. Built by `sdkConfigFor`
398
+ * or `sdkConfigFromEnv`; a site with an unusual setup (a dev proxy, a staging hub) writes the literal.
399
+ */
400
+ type SdkConfig = Extract<WebsiteSDKConfig, {
401
+ backend: "v9";
402
+ }> | (Exclude<WebsiteSDKConfig, {
403
+ backend: "v9";
404
+ }> & {
405
+ readonly backend: "v10";
406
+ });
407
+ /** The SDK options a surface may need, as data (`translationOverrides`, `customAttributes`,
408
+ * `rentalHighlightPrioritization`, …). `searchAllRentalsAtOnce` is excluded: it changes the SDK's
409
+ * type and is chosen with `SdkMode`. */
410
+ type SdkOptions = Omit<WebsiteSDKOptions, "searchAllRentalsAtOnce">;
411
+ /** What a site hands to every surface (`src/data/sdk.ts`). Plain data: an Astro island receives it as
412
+ * a prop, so pass only the options the site uses — every byte is serialized into each island. */
413
+ interface SdkSetup {
414
+ /** Backend, endpoints and the public token. */
415
+ readonly config: SdkConfig;
416
+ /** SDK options; absent = the SDK's defaults. */
417
+ readonly options?: SdkOptions;
418
+ }
419
+ /** "default": paged search plus `mapSearch`. "all-rentals-at-once": `searchAllRentalsAtOnce: true`
420
+ * (at most 100 rentals priced in one call, no cursor, no `mapSearch`). */
421
+ type SdkMode = "default" | "all-rentals-at-once";
422
+ /** What every SDK-using store takes first: an instance, or a getter (lazy, or keyed by React). A
423
+ * store calls a getter once, on its first request, and again only after a failed resolve. */
424
+ type SdkSource = WebsiteSdk | (() => WebsiteSdk | Promise<WebsiteSdk>);
425
+ /** The instance behind a source. Call it once per call site and keep the result. */
426
+ type ResolveSdk = (source: SdkSource) => Promise<WebsiteSdk>;
427
+ /** See {@link ResolveSdk}. */
428
+ declare const resolveSdk: ResolveSdk;
429
+ //#endregion
430
+ //#region src/shared/dates.d.ts
431
+ /** A local calendar day, YYYY-MM-DD. */
432
+ type IsoDay = string;
433
+ /** A calendar month, YYYY-MM (the URL spells it MM-YYYY). */
434
+ type IsoMonth = string;
435
+ /** A real calendar day in YYYY-MM-DD ("2026-02-30" is not). */
436
+ type IsIsoDay = (value: unknown) => value is IsoDay;
437
+ /** Today in the visitor's timezone (`now` is injected in tests). */
438
+ type TodayIso = (now?: Date) => IsoDay;
439
+ /** `day` moved by `days` calendar days (negative goes back). Throws a RangeError for a non-ISO day. */
440
+ type AddDays = (day: IsoDay, days: number) => IsoDay;
441
+ /** Whole nights from `start` to `end` (negative when reversed). Throws a RangeError for a non-ISO day. */
442
+ type NightsBetween = (start: IsoDay, end: IsoDay) => number;
443
+ /** "2026-09-01" → "01.09.2026" (de-DE) / "09/01/2026" (en-US) via Intl; non-ISO input returned unchanged. */
444
+ type FormatIsoDate = (day: IsoDay, locale: Locale) => string;
445
+ /** See {@link IsIsoDay}. */
446
+ declare const isIsoDay: IsIsoDay;
447
+ /** See {@link TodayIso}. */
448
+ declare const todayIso: TodayIso;
449
+ /** See {@link AddDays}. */
450
+ declare const addDays: AddDays;
451
+ /** See {@link NightsBetween}. */
452
+ declare const nightsBetween: NightsBetween;
453
+ /** See {@link FormatIsoDate}. */
454
+ declare const formatIsoDate: FormatIsoDate;
455
+ //#endregion
456
+ //#region src/shared/glyphs.d.ts
457
+ /** A glyph name. */
458
+ type Glyph = "users" | "bed" | "bath" | "ruler" | "maximize" | "sofa" | "house" | "door" | "waves" | "sailboat" | "footprints" | "mountain" | "building" | "tractor" | "pin" | "paw" | "flame" | "droplets" | "trees" | "sun" | "armchair" | "home" | "key" | "star" | "check" | "view" | "terrace" | "balcony" | "garden" | "grill" | "fireplace" | "pool" | "sauna" | "jetty" | "kitchen" | "oven" | "microwave" | "fridge" | "freezer" | "coffee" | "kettle" | "dishwasher" | "washer" | "dryer" | "heating" | "wifi" | "tv" | "elevator" | "parking" | "garage" | "children" | "nonSmoking" | "groups" | "noGroups" | "pets" | "beach";
459
+ /** The type of `GLYPHS` (declared below): every `Glyph`, each once, in the order of the union. */
460
+ type GlyphList = readonly Glyph[];
461
+ /** Every glyph name. */
462
+ declare const GLYPHS: GlyphList;
463
+ //#endregion
464
+ //#region src/shared/price-label.d.ts
465
+ /**
466
+ * How a formatted price string is trimmed:
467
+ * - "show": verbatim.
468
+ * - "hide-zero": drop a zero cents group only ("1.234,00 €" → "1.234 €"; "1.234,50 €" stays).
469
+ * - "truncate": cut the cents, never rounding up ("2.022,80 €" → "2.022 €"), so a card never shows more
470
+ * than the checkout.
471
+ * - "round": half up to whole units ("2.022,50 €" → "2.023 €").
472
+ */
473
+ type PriceCents = "show" | "hide-zero" | "truncate" | "round";
474
+ /** The last separator followed by exactly two digits is the decimal part; exactly three trailing digits
475
+ * group thousands; "CHF 1'234.50", "1,234.50 €", "120.00 EUR" read. A shape it does not recognise (no
476
+ * number, two numbers, one decimal digit) is returned unchanged — never turned into a wrong figure. */
477
+ type FormatPriceLabel = (label: string, cents: PriceCents) => string;
478
+ /** A formatted total → a number for ordering only, or null when unreadable (never 0: 0 is a price and
479
+ * would sort first). */
480
+ type ParsePriceLabel = (label: string | null | undefined) => number | null;
481
+ /** See {@link FormatPriceLabel}. */
482
+ declare const formatPriceLabel: FormatPriceLabel;
483
+ /** See {@link ParsePriceLabel}. */
484
+ declare const parsePriceLabel: ParsePriceLabel;
485
+ //#endregion
486
+ //#region src/cards/match.d.ts
487
+ /** An inclusive numeric bound; either side may be open. */
488
+ interface Range {
489
+ /** Lowest value, inclusive; absent = open. */
490
+ min?: number;
491
+ /** Highest value, inclusive; absent = open. */
492
+ max?: number;
493
+ }
494
+ /** One fact a rental states or does not. */
495
+ type CardSignal =
496
+ /** A boolean in `CardSource.flags` ("petsAllowed", "atBeach" …); unknown or false → no match. */
497
+ {
498
+ flag: string;
499
+ } |
500
+ /** An exact highlight, attribute or vicinity item (`highlights` ∪ `stated`), trimmed equality. */
501
+ {
502
+ stated: string;
503
+ } | {
504
+ guests: Range;
505
+ } | {
506
+ bedrooms: Range;
507
+ } |
508
+ /** The feed's `rentalType`, verbatim. */
509
+ {
510
+ rentalType: string;
511
+ } |
512
+ /** Real guest ratings only; a rental without a rating never matches. */
513
+ {
514
+ rating: {
515
+ min?: number;
516
+ minReviews?: number;
517
+ };
518
+ } |
519
+ /** Exact `location` values (towns, or the district a site writes there). */
520
+ {
521
+ location: readonly string[];
522
+ } |
523
+ /** Any of the site's region values (`CardSource.regions`). */
524
+ {
525
+ region: readonly string[];
526
+ } | {
527
+ ids: readonly string[];
528
+ } |
529
+ /** A free numeric fact (`CardSource.numbers[key]`) in a range; absent = no match. */
530
+ {
531
+ number: {
532
+ key: string;
533
+ } & Range;
534
+ };
535
+ /** A signal or a combination: `anyOf` is the themed-row OR (flag OR vicinity item), `allOf`
536
+ * the "Sauna AND Kamin" case. An empty `anyOf` matches nothing, an empty `allOf` everything. */
537
+ type CardMatch = CardSignal | {
538
+ anyOf: readonly CardMatch[];
539
+ } | {
540
+ allOf: readonly CardMatch[];
541
+ };
542
+ type Matches = (source: CardSource, match: CardMatch) => boolean;
543
+ /** See {@link Matches}. */
544
+ declare const matches: Matches;
545
+ //#endregion
546
+ //#region src/cards/model.d.ts
547
+ /** One image as a card renders it. */
548
+ interface CardImage {
549
+ /** The image URL (WebP from the build). */
550
+ src: string;
551
+ /** Responsive candidates, or null. */
552
+ srcset?: string | null;
553
+ /** null: the component fills its "{name}, Bild {n}" template (headless carries no language). */
554
+ alt: string | null;
555
+ }
556
+ /** A real guest rating summary. */
557
+ interface CardRating {
558
+ /** On `scale`. */
559
+ value: number;
560
+ /** Reviews the score is the mean of. */
561
+ count: number;
562
+ /** Default 5. */
563
+ scale?: number;
564
+ }
565
+ /** A priced stay, verbatim from the SDK. */
566
+ interface StayPrice {
567
+ /** `formattedTotal`: the stay total, never a per-night figure. */
568
+ readonly total: string;
569
+ /** `discount.formattedOriginalTotal` — the struck price; gates on its own field. */
570
+ readonly original: string | null;
571
+ /** Presence of the SDK discount block — the offer chip's only source. */
572
+ readonly discounted: boolean;
573
+ /** `discount.applied[].label`, the PMS's own words (never an amount). */
574
+ readonly offerLabels: readonly string[];
575
+ }
576
+ /** The concrete stay a price is for when it is not the searched dates. */
577
+ interface StayWindow {
578
+ /** Arrival of the priced stay. */
579
+ readonly start: IsoDay;
580
+ /** Departure of the priced stay. */
581
+ readonly end: IsoDay;
582
+ /** Its nights, or null when the SDK states none. */
583
+ readonly nights: number | null;
584
+ /** `formatStayRange` of the window. */
585
+ readonly label: string;
586
+ /** Why it differs from the search: the guest's own dates inside a ± window, the stay the hub chose for a window, or an alternative period. */
587
+ readonly source: "own-dates" | "hub-chosen" | "alternative";
588
+ }
589
+ /** One bookable window suggested for a non-exact result, best first. */
590
+ interface AlternativePeriod {
591
+ /** Arrival. */
592
+ readonly start: IsoDay;
593
+ /** Departure. */
594
+ readonly end: IsoDay;
595
+ /** Its nights, or null when the SDK states none. */
596
+ readonly nights: number | null;
597
+ /** `formatStayRange` of the window. */
598
+ readonly label: string;
599
+ /** The rental link with this window handed over. */
600
+ readonly href: string;
601
+ /** Its price, or null when the SDK priced none. */
602
+ readonly price: StayPrice | null;
603
+ }
604
+ /** The one card input: catalogue fields (build) + optional live fields (results). Plain JSON,
605
+ * so it crosses an island boundary. */
606
+ interface CardSource {
607
+ /** PMS rental id; keys the heart, the map marker and selection. */
608
+ id: string;
609
+ /** The rental's name. */
610
+ name: string;
611
+ /** The site's own detail URL (+ handover query on results). */
612
+ href: string;
613
+ /** The town (or the site's district line); null renders none. */
614
+ location?: string | null;
615
+ /** For `spreadBy: "street"`. */
616
+ street?: string | null;
617
+ /** The site's region values. */
618
+ regions?: readonly string[];
619
+ /** The photos, in feed order. */
620
+ images: readonly CardImage[];
621
+ /** Guests, bedrooms, bathrooms, area; absence is undefined, never 0. */
622
+ counts?: RentalCounts;
623
+ /** The PMS distances (water, centre, ski …). */
624
+ distances?: readonly RentalDistance[];
625
+ /** Pill candidates in lead order (`rankHighlights` order); may include count echoes (the
626
+ * model drops them by shape). */
627
+ highlights?: readonly string[];
628
+ /** Further exact strings for `{ stated }` matching only (attribute and vicinity items). Never
629
+ * rendered. */
630
+ stated?: readonly string[];
631
+ /** The owner's own labels; they always lead the pill row, outside `pills.only`, `prefer` and
632
+ * `exclude` (still deduped against the badges, renamed, counted in `limit`). */
633
+ ownerPills?: readonly string[];
634
+ /** PMS booleans (`RentalFlag`) and site-derived flags (browser filters). */
635
+ flags?: Readonly<Record<string, boolean>>;
636
+ /** Multi-valued facets (region, property, room type, floor …) for facet filters and scopes. */
637
+ facets?: Readonly<Record<string, readonly string[]>>;
638
+ /** Free numeric facts the site derives (classification stars, floors …) for `minimum` filters
639
+ * and `{ number }` matching; never rendered by the card. */
640
+ numbers?: Readonly<Record<string, number>>;
641
+ /** The PMS rental type, verbatim ("Ferienwohnung"). */
642
+ rentalType?: string | null;
643
+ /** The guest rating summary, or null without reviews. */
644
+ rating?: CardRating | null;
645
+ /** One editorial line (an owner's sentence). Site data, never generated. */
646
+ blurb?: string | null;
647
+ /** Where the rental is, or null (the map and the viewport filter skip it). */
648
+ coords?: {
649
+ readonly lat: number;
650
+ readonly lng: number;
651
+ } | null;
652
+ /** The property (Anlage) it belongs to, or null. */
653
+ propertyId?: string | null;
654
+ /** Live: the priced stay (results only). */
655
+ price?: StayPrice | null;
656
+ /** Live: the stay's nights. */
657
+ nights?: number | null;
658
+ /** Live: the concrete stay when it is not the searched dates. */
659
+ stay?: StayWindow | null;
660
+ /** A non-exact date match. */
661
+ alternative?: boolean;
662
+ /** Live: the bookable windows of a non-exact match, best first. */
663
+ periods?: readonly AlternativePeriod[];
664
+ }
665
+ /** Live-only keys of CardSource. */
666
+ type CardLiveField = "price" | "nights" | "stay" | "alternative" | "periods";
667
+ /** Build-time facts per rental for the results (what the live projection lacks). */
668
+ type RentalFacts = Partial<Omit<CardSource, "id" | CardLiveField>>;
669
+ /** Facts by rental id (`rentalFactsMap`), passed to SearchResults. */
670
+ type RentalFactsMap = Readonly<Record<string, RentalFacts>>;
671
+ /** A fact the card can state, in the order a site lists them. */
672
+ type FactKind = "guests" | "bedrooms" | "bathrooms" | "area" | "distance";
673
+ /** One fact a card states: a count, or the distance (with whether it is adjacent, under 25 m). */
674
+ type CardFact = {
675
+ kind: "guests" | "bedrooms" | "bathrooms" | "area";
676
+ value: number;
677
+ } | {
678
+ kind: "distance";
679
+ distance: RentalDistance;
680
+ adjacent: boolean;
681
+ };
682
+ /** A pill: a label, the distance moved to the row (`distance.as: "pill"`), or a fact a renderer
683
+ * places in the row (`factStyle: "pills"` is the renderer's choice; the model lists facts). */
684
+ type CardPill = {
685
+ kind: "label";
686
+ label: string;
687
+ icon: Glyph | null;
688
+ } | {
689
+ kind: "distance";
690
+ distance: RentalDistance;
691
+ adjacent: boolean;
692
+ } | {
693
+ kind: "fact";
694
+ fact: CardFact;
695
+ };
696
+ /** A claim over the photo, from a `BadgeRule`. */
697
+ interface CardBadge {
698
+ /** The claim's text (the rule's label). */
699
+ label: string;
700
+ /** The claim's glyph, or null. */
701
+ icon: Glyph | null;
702
+ }
703
+ /** One query-relative chip by fixed priority: offer > alternative-period (exact searches only).
704
+ * No "new": the search states no listing date. */
705
+ type CardStatus = {
706
+ kind: "offer";
707
+ campaign: string | null;
708
+ } | {
709
+ kind: "alternative-period";
710
+ };
711
+ /** How the pill row is filled: owner pills first, then the curated feed highlights. */
712
+ interface PillOptions {
713
+ /** Pills at most. Default 3; 0 = no row. */
714
+ limit?: number;
715
+ /** Fill from highlights after the owner pills. Default true; false = owner pills only. */
716
+ fill?: boolean;
717
+ /** Exclusive allowlist of FEED highlights in display order, exact equality ("Backofen" is not
718
+ * "Kamin/Ofen"). Owner pills are the owner's statement and always lead, outside the list. */
719
+ only?: readonly string[];
720
+ /** These feed highlights lead the feed part in this order, the rest follow in source order. */
721
+ prefer?: readonly string[];
722
+ /** Feed highlights never shown as a pill (an owner pill of the same label still shows: a graded
723
+ * owner "Meerblick" replaces the feed's). */
724
+ exclude?: readonly string[];
725
+ /** Display names; matching uses the feed label ("Kamin/Ofen" → "Kamin"). */
726
+ rename?: Readonly<Record<string, string>>;
727
+ /** A glyph per feed label. Absent: text only (a glyph may not carry a meaning its label does not). */
728
+ glyphMap?: Readonly<Record<string, Glyph>>;
729
+ }
730
+ /** A photo claim from a signal the PMS states — never a distance threshold for "am Strand". */
731
+ interface BadgeRule {
732
+ /** The PMS signal that earns the badge. */
733
+ when: CardMatch;
734
+ /** The badge's text. */
735
+ label: string;
736
+ /** The badge's glyph. Default none. */
737
+ icon?: Glyph;
738
+ /** One badge per group, the first rule that matches ("Meerblick" beats "Seeblick"). */
739
+ group?: string;
740
+ }
741
+ /** Behaviour options of the card model (components' RentalCardOptions extends this). */
742
+ interface CardModelOptions {
743
+ /** Default ["guests", "bedrooms", "area", "distance"]; omit "distance" for "no water on the card". */
744
+ facts?: readonly FactKind[];
745
+ /** Which distance (default WATER_KINDS by priority) and where: in the facts (default) or as pill 0. */
746
+ distance?: {
747
+ prefer?: readonly DistanceKind[];
748
+ as?: "fact" | "pill";
749
+ };
750
+ /** false: no pill row. */
751
+ pills?: PillOptions | false;
752
+ /** Default none; `limit` default 2. */
753
+ badges?: {
754
+ rules: readonly BadgeRule[];
755
+ limit?: number;
756
+ };
757
+ /** Images kept, default 5; 1 = a still. */
758
+ maxImages?: number;
759
+ /** Default "hide-zero"; applies to total, original and every period price. */
760
+ priceCents?: PriceCents;
761
+ /** Default "footer": PMS offer labels in the price foot; "pills": a row of them; "badge": the
762
+ * status chip names the first label instead of the generic "Angebot". */
763
+ offer?: "footer" | "pills" | "badge";
764
+ }
765
+ /** What `toCardModel` needs to know about the query, not the rental. */
766
+ interface CardContext {
767
+ /** True only for an exact start+end search; gates the alternative-period chip. */
768
+ exactSearch?: boolean;
769
+ }
770
+ /** Everything a renderer draws. */
771
+ interface RentalCardModel {
772
+ /** PMS rental id. */
773
+ id: string;
774
+ /** The rental's name. */
775
+ name: string;
776
+ /** The detail URL (with the handover query on results). */
777
+ href: string;
778
+ /** The location line, or null. */
779
+ location: string | null;
780
+ /** The editorial line, or null. */
781
+ blurb: string | null;
782
+ /** The photos kept (`maxImages`). */
783
+ images: readonly CardImage[];
784
+ /** The facts in the site's order, each only when stated. */
785
+ facts: readonly CardFact[];
786
+ /** The pill row, capped at `limit`. */
787
+ pills: readonly CardPill[];
788
+ /** The photo claims, capped at `badges.limit`. */
789
+ badges: readonly CardBadge[];
790
+ /** The one query-relative chip, or null. */
791
+ status: CardStatus | null;
792
+ /** The rating summary, or null. */
793
+ rating: CardRating | null;
794
+ /** `formatPriceLabel(…, priceCents)` applied; `original` is null when its formatted label equals
795
+ * the formatted total (rounding made them equal — a struck price equal to the total states
796
+ * nothing). */
797
+ price: StayPrice | null;
798
+ /** The priced stay's nights, or null. */
799
+ nights: number | null;
800
+ /** The concrete stay when it is not the searched dates, or null. */
801
+ stay: StayWindow | null;
802
+ /** The alternative windows, best first. */
803
+ periods: readonly AlternativePeriod[];
804
+ /** The PMS offer labels of the price, verbatim (never an amount). */
805
+ offerLabels: readonly string[];
806
+ }
807
+ type ToCardModel = (source: CardSource, options?: CardModelOptions, context?: CardContext) => RentalCardModel;
808
+ /** See {@link ToCardModel}. */
809
+ declare const toCardModel: ToCardModel;
810
+ //#endregion
811
+ //#region src/shared/routes.d.ts
812
+ /** A route with `{slug}` and/or `{id}` tokens ("/unterkuenfte/{slug}", "/objekt/{id}/"); used verbatim,
813
+ * trailing slashes kept. */
814
+ type RouteTemplate = string;
815
+ /** The values a template's tokens take. */
816
+ interface RouteParams {
817
+ /** The rental's slug (from the catalogue pass; no runner derives one). */
818
+ readonly slug?: string;
819
+ /** The rental's id. */
820
+ readonly id?: string;
821
+ }
822
+ /** Fill a template. Throws when a token stays unfilled or empty, or is unknown (every `{…}` is a token),
823
+ * or a brace is unbalanced: a wrong link must fail the build, not ship. */
824
+ type RouteHref = (template: RouteTemplate, params: RouteParams) => string;
825
+ /** See {@link RouteHref}. */
826
+ declare const routeHref: RouteHref;
827
+ //#endregion
828
+ //#region src/cards/source.d.ts
829
+ /** What the rental model does not know: the site's route and its own data. */
830
+ interface CardSourceExtras {
831
+ /** The detail URL. One of `href` / `rentalHref` is required (throws otherwise — there is no
832
+ * route default). */
833
+ href?: string;
834
+ /** Template filled with the rental's slug and id when `href` is absent ("/unterkuenfte/{slug}"). */
835
+ rentalHref?: RouteTemplate;
836
+ /** The owner's own labels: they lead the pill row, outside `pills.only` / `exclude`. */
837
+ ownerPills?: readonly string[];
838
+ /** Site-derived highlight labels (a seal synthesised from a flag), placed before the PMS's. Unlike
839
+ * owner pills they are feed labels: `pills.only`, `prefer` and `exclude` place and filter them. */
840
+ highlights?: readonly string[];
841
+ /** The card's one-line description. */
842
+ blurb?: string;
843
+ /** Overrides `town` (a district, a display-name rule). */
844
+ location?: string;
845
+ /** The rental's region values (the search bar's `?region=` matches them). */
846
+ regions?: readonly string[];
847
+ /** Site-derived flags, merged over the PMS flags. */
848
+ flags?: Readonly<Record<string, boolean>>;
849
+ /** Site facets (a filter's values per facet key). */
850
+ facets?: Readonly<Record<string, readonly string[]>>;
851
+ /** Site numbers (a filter's minimum per key). */
852
+ numbers?: Readonly<Record<string, number>>;
853
+ }
854
+ /** A card source from the rental model: counts, distances, the rating summary, `stated` from the
855
+ * amenity, theme and vicinity items, the (WebP) photos with their captions as alt text. */
856
+ type CardSourceOf = (rental: RentalDetail, extras?: CardSourceExtras) => CardSource;
857
+ /** The facts map for SearchResults, keyed by id: live keys dropped, and `images` dropped too —
858
+ * every live result carries its own photos, and the map ships on every search page. */
859
+ type RentalFactsMapOf = (sources: readonly CardSource[]) => RentalFactsMap;
860
+ /** Live fields win where they state a value (not undefined, not null); facts fill the gaps. One
861
+ * exception: non-empty `highlights` of the facts win, because the build ranked the same PMS labels
862
+ * (`rankHighlights`) and a result card must show the pills its showcase card shows; the live list is
863
+ * the fallback for rentals without facts. */
864
+ type WithRentalFacts = (live: CardSource, facts: RentalFacts | undefined) => CardSource;
865
+ /** The facts map the build wrote (`rentalFactsMap`) as JSON at `url`: the status and the shape are
866
+ * checked (an object keyed by rental id), so a missing or broken file is an error, never an empty
867
+ * facts set. */
868
+ type LoadRentalFacts = (url: string) => Promise<RentalFactsMap>;
869
+ /** See {@link CardSourceOf}. */
870
+ declare const cardSource: CardSourceOf;
871
+ /** See {@link RentalFactsMapOf}. */
872
+ declare const rentalFactsMap: RentalFactsMapOf;
873
+ /** See {@link WithRentalFacts}. */
874
+ declare const withRentalFacts: WithRentalFacts;
875
+ /** See {@link LoadRentalFacts}. */
876
+ declare const loadRentalFacts: LoadRentalFacts;
877
+ //#endregion
878
+ //#region src/sdk/endpoints.d.ts
879
+ /** The v10 endpoint set of one stage. */
880
+ interface VofficeEndpoints {
881
+ /** GraphQL API. */
882
+ readonly apiEndpoint: string;
883
+ /** Image host the SDK builds image URLs on. */
884
+ readonly imageBaseUrl: string;
885
+ /** Full REST search endpoint. */
886
+ readonly searchEndpoint: string;
887
+ }
888
+ /** The be-on! Hub endpoint set of one stage (v9); the API URLs derive from `apiBaseUrl`. */
889
+ interface HubEndpoints {
890
+ /** Origin of the Hub's GraphQL and REST APIs. */
891
+ readonly apiBaseUrl: string;
892
+ /** Image proxy the SDK builds image URLs on. */
893
+ readonly imageProxyBaseUrl: string;
894
+ }
895
+ /** The type of `VOFFICE_ENDPOINTS` (declared below): public v10 endpoints per stage. */
896
+ type VofficeEndpointTable = Readonly<Record<Stage, VofficeEndpoints>>;
897
+ /** The type of `HUB_ENDPOINTS` (declared below): be-on! Hub endpoints per stage. */
898
+ type HubEndpointTable = Readonly<Record<Stage, HubEndpoints>>;
899
+ /** The three values a site decides. */
900
+ interface SdkConfigForInput {
901
+ /** Which PMS answers. */
902
+ readonly backend: Backend;
903
+ /** Which environment of it. */
904
+ readonly stage: Stage;
905
+ /** v9: the be-on key (public, origin-bound). v10: the frontend access token. */
906
+ readonly token: string;
907
+ }
908
+ /** Backend + stage + token → a full `SdkConfig` with the platform's endpoints. Extras are spread on top
909
+ * by the caller: `{ ...sdkConfigFor(…), facilityObjectGroupRelationAttributeId: 18962 }`. */
910
+ type SdkConfigFor = (input: SdkConfigForInput) => SdkConfig;
911
+ /** Public v10 endpoints per stage. */
912
+ declare const VOFFICE_ENDPOINTS: VofficeEndpointTable;
913
+ /** be-on! Hub endpoints per stage (https only). */
914
+ declare const HUB_ENDPOINTS: HubEndpointTable;
915
+ /** See {@link SdkConfigFor}. */
916
+ declare const sdkConfigFor: SdkConfigFor;
917
+ //#endregion
918
+ //#region src/sdk/env.d.ts
919
+ /** The env names the fleet (kit lineage) and the platform builder use, plus the optional `CMS_STAGE`. */
920
+ declare const SDK_ENV: {
921
+ /** "v9" or "v10". Required: there is no silent default. */
922
+ readonly backend: "CMS_BACKEND";
923
+ /** Optional. Set it ("development" | "production") to use the platform endpoints instead of the URL
924
+ * variables. */
925
+ readonly stage: "CMS_STAGE";
926
+ /** v9 (be-on! Hub). */
927
+ readonly v9: {
928
+ readonly token: "HUB_API_KEY";
929
+ readonly graphqlUrl: "HUB_GRAPHQL_URL";
930
+ readonly v0ApiBaseUrl: "HUB_V0_API_BASE_URL";
931
+ readonly v1ApiBaseUrl: "HUB_V1_API_BASE_URL";
932
+ readonly imageProxyBaseUrl: "IMAGE_PROXY_BASE_URL";
933
+ };
934
+ /** v10. */
935
+ readonly v10: {
936
+ readonly token: "VOFFICE_LOCAL_DEV_ACCESS_TOKEN";
937
+ readonly apiEndpoint: "VOFFICE_API_ENDPOINT";
938
+ readonly searchEndpoint: "VOFFICE_SEARCH_ENDPOINT";
939
+ readonly imageBaseUrl: "VOFFICE_IMAGE_BASE_URL";
940
+ };
941
+ };
942
+ /** Any env-shaped record: `import.meta.env`, `process.env`, a test literal. Only non-empty strings count
943
+ * as set (Vite's boolean `DEV`/`PROD` flags are ignored). */
944
+ type EnvRecord = Readonly<Record<string, string | boolean | undefined>>;
945
+ /**
946
+ * The fleet's env names → an `SdkConfig`. Rules, in order:
947
+ * 1. `CMS_BACKEND` must be "v9" or "v10" — no silent default (the kits' client-side "unset reads as
948
+ * v9" is a capability-gating fallback, not a construction rule).
949
+ * 2. The backend's token is required.
950
+ * 3. With `CMS_STAGE` set, the platform endpoints of that stage are used and the URL variables must be
951
+ * absent (two sources for one value is an error, not a merge).
952
+ * 4. Otherwise every URL variable of the backend is required.
953
+ * Throws one `SdkEnvError` naming EVERY missing or conflicting variable at once.
954
+ */
955
+ type SdkConfigFromEnv = (env: EnvRecord) => SdkConfig;
956
+ /** The env cannot build a config. Names only: a value never reaches the message. */
957
+ declare class SdkEnvError extends Error {
958
+ /** Required names that are unset. */
959
+ readonly missing: readonly string[];
960
+ /** Names that are set but unusable: a URL variable beside `CMS_STAGE`, or a value outside its
961
+ * vocabulary (`CMS_BACKEND`, `CMS_STAGE`). */
962
+ readonly conflicting: readonly string[];
963
+ constructor(missing: readonly string[], conflicting: readonly string[]);
964
+ }
965
+ /** See {@link SdkConfigFromEnv}. */
966
+ declare const sdkConfigFromEnv: SdkConfigFromEnv;
967
+ //#endregion
968
+ //#region src/sdk/create.d.ts
969
+ /** Construct an SDK the caller owns and disposes (`await sdk.dispose()`): build-time loaders and
970
+ * scripts. Browser surfaces use `shareSdk`/`loadSharedSdk`. The API is rate-limited (1 request per
971
+ * second per instance), so a long-lived caller keeps ONE instance. */
972
+ type CreateSdk = (setup: SdkSetup, mode?: SdkMode) => WebsiteSdk;
973
+ /** See {@link CreateSdk}. */
974
+ declare const createSdk: CreateSdk;
975
+ //#endregion
976
+ //#region src/sdk/share.d.ts
977
+ /** A shared instance and the caller's claim on it. */
978
+ interface SharedSdk {
979
+ /** The page's shared instance for the setup. */
980
+ readonly sdk: WebsiteSdk;
981
+ /** Drop this claim (idempotent). The instance is disposed when the last claim is released and no new
982
+ * claim arrived within the same task (a React StrictMode remount reuses it). */
983
+ release(): void;
984
+ }
985
+ /** Claim the page's instance for (setup, mode) — keyed by stable JSON, so key order in `setup` does not
986
+ * matter and different options or modes get different instances. Ref-counted. */
987
+ type ShareSdk = (setup: SdkSetup, mode?: SdkMode) => SharedSdk;
988
+ /** See {@link ShareSdk}. */
989
+ declare const shareSdk: ShareSdk;
990
+ //#endregion
991
+ //#region src/sdk/load.d.ts
992
+ /** `shareSdk` with the website SDK imported on first call (a dynamic `import()`), so a page whose only
993
+ * SDK use is a submit (contact) ships no SDK code up front. Resolves to the same instance `shareSdk`
994
+ * holds for an equal (setup, mode). */
995
+ type LoadSharedSdk = (setup: SdkSetup, mode?: SdkMode) => Promise<SharedSdk>;
996
+ /** See {@link LoadSharedSdk}. */
997
+ declare const loadSharedSdk: LoadSharedSdk;
998
+ //#endregion
999
+ //#region src/sdk/search-pages.d.ts
1000
+ /** Results per request. Below the v9 hub's per-request price cap of 128 (positions 0–127 were measured
1001
+ * priced at limit 1000, the rest unpriced), and constant within a walk because the v9 cursor is a page
1002
+ * number at this size. */
1003
+ declare const SEARCH_PAGE_SIZE: 100;
1004
+ /** The SDK's raw answer for one page (callers map it). */
1005
+ type RawSearchPage = Awaited<ReturnType<WebsiteSdk["live"]["search"]["search"]>>;
1006
+ /** One search, as every page of a walk repeats it. */
1007
+ interface SearchPageRequest {
1008
+ /** The query string exactly as the SDK takes it (callers clean it: `toSdkQuery`, or a one-key probe). */
1009
+ readonly query: string;
1010
+ /** Language of the PMS strings in the answer. */
1011
+ readonly locale: Locale;
1012
+ /** The backend's ordering; absent = the backend's own. A field sort is v10 only. */
1013
+ readonly sort?: SearchSearchSort;
1014
+ /** v10 only: server-side id restriction. A v9 request carrying it is refused (the v9 hub has no id
1015
+ * filter; the caller filters the items itself). */
1016
+ readonly rentalIdsIn?: readonly string[];
1017
+ /** The mode the SDK was created in (pass the same value as to `createSdk` / `shareSdk`): "all-rentals-at-once"
1018
+ * is one call, no cursor, no limit. Default "default". */
1019
+ readonly sdkMode?: SdkMode;
1020
+ }
1021
+ /** Limits of one walk. */
1022
+ interface WalkOptions {
1023
+ /** Stop after this many pages and report `truncated`. Default: no cap — a walk is never silently cut. */
1024
+ readonly maxPages?: number;
1025
+ /** Abort between pages; the walk rejects with the signal's reason. */
1026
+ readonly signal?: AbortSignal;
1027
+ }
1028
+ /** How a walk ended. */
1029
+ interface WalkSummary {
1030
+ /** Pages requested. */
1031
+ readonly pages: number;
1032
+ /** The backend had more pages than the walk read (`maxPages` reached, or a next page without a cursor). */
1033
+ readonly truncated: boolean;
1034
+ }
1035
+ /** One request (limit `SEARCH_PAGE_SIZE` unless `sdkMode` is "all-rentals-at-once"). A failure is logged through
1036
+ * `logSdkFailure("search.search")` and rethrown. */
1037
+ type FetchSearchPage = (sdk: WebsiteSdk, request: SearchPageRequest, cursor?: string | null) => Promise<RawSearchPage>;
1038
+ /** Every page in order, strictly sequentially (the SDK's queue rate-limits; two requests in flight would
1039
+ * only queue). Stops when the backend reports no next page, or at `maxPages`. */
1040
+ type WalkSearchPages = (sdk: WebsiteSdk, request: SearchPageRequest, options?: WalkOptions) => AsyncGenerator<RawSearchPage, WalkSummary>;
1041
+ /** See {@link FetchSearchPage}. */
1042
+ declare const fetchSearchPage: FetchSearchPage;
1043
+ /** See {@link WalkSearchPages}. */
1044
+ declare const walkSearchPages: WalkSearchPages;
1045
+ //#endregion
1046
+ //#region src/rental/adapt.d.ts
1047
+ /** How one catalogue rental becomes a `RentalDetail`. */
1048
+ interface AdaptRentalOptions {
1049
+ /** The locale `getRentals` ran with: decides the month names the review order reads. */
1050
+ locale: Locale;
1051
+ /** From `assignSlugs`. */
1052
+ slug: string;
1053
+ /** From `fetchRentalFlags`; omitted flags stay absent. */
1054
+ flags?: Partial<Record<RentalFlag, boolean>>;
1055
+ /** "light" (default): `**bold**` and `- `/`* ` list lines become structure, so no asterisk
1056
+ * reaches a guest. "plain": text verbatim. */
1057
+ markup?: "light" | "plain";
1058
+ /** Publish `description.arrivalInfo`. Default false: on v9 tenants this text carried key-safe
1059
+ * codes on every public page. Turn it on only after reading the feed. */
1060
+ publishArrivalInfo?: boolean;
1061
+ /** Append the property's photos after the rental's own. Default "omit". */
1062
+ propertyImages?: "append" | "omit";
1063
+ /** Category labels dropped from `amenities`, besides `ALWAYS_EXCLUDED_CATEGORIES`. */
1064
+ excludeCategories?: readonly string[];
1065
+ /** Room attribute prefixes never shown, besides `HIDDEN_ROOM_ATTRIBUTES`. */
1066
+ hideRoomAttributes?: readonly string[];
1067
+ /** Patterns no published string may match. A match throws `RentalContentError` naming the
1068
+ * rental id and the field — never the matched text, which may be a secret. */
1069
+ forbid?: readonly RegExp[];
1070
+ /** Build-time notes. Default: one `console.warn` per kind and process. */
1071
+ report?: (event: AdaptEvent) => void;
1072
+ }
1073
+ /** A build-time note of the adapter (nothing a guest sees). */
1074
+ type AdaptEvent =
1075
+ /** The feed listed the same photo more than once (v9 `.jpg`/`.jpeg`, extensionless copies). */
1076
+ {
1077
+ kind: "duplicate-images";
1078
+ rentalId: string;
1079
+ dropped: number;
1080
+ } |
1081
+ /** A floor plan URL had no `rs:fill` rendition to rewrite; it may render cropped. */
1082
+ {
1083
+ kind: "floorplan-not-rewritten";
1084
+ rentalId: string;
1085
+ } |
1086
+ /** A review's stay month did not read in `locale`; the feed's order was kept. */
1087
+ {
1088
+ kind: "review-order-kept";
1089
+ rentalId: string;
1090
+ };
1091
+ /** Thrown when a published string matches `forbid`. */
1092
+ declare class RentalContentError extends Error {
1093
+ /** The rental whose content matched. */
1094
+ readonly rentalId: string;
1095
+ /** Model path of the offending string, e.g. "amenities[3].items[2]". */
1096
+ readonly field: string;
1097
+ constructor(rentalId: string, field: string);
1098
+ }
1099
+ type AdaptRental = (raw: CatalogueRental, options: AdaptRentalOptions) => RentalDetail;
1100
+ /** See {@link AdaptRental}. */
1101
+ declare const adaptRental: AdaptRental;
1102
+ //#endregion
1103
+ //#region src/rental/amenities.d.ts
1104
+ /** Category labels (de-DE and en-US) never published: DESCRIPTION repeats prose, SEO metadata
1105
+ * and arrival instructions; SYSTEM holds codes. */
1106
+ declare const ALWAYS_EXCLUDED_CATEGORIES: readonly string[];
1107
+ /** One row of the amenity preview: a highlight, a distance fact, or the dog flag. */
1108
+ type AmenityPreviewItem = {
1109
+ kind: "highlight";
1110
+ label: string;
1111
+ } | {
1112
+ kind: "distance";
1113
+ distance: RentalDistance;
1114
+ } |
1115
+ /** The rental's `petsAllowed` flag is true (the UI says "Hunde willkommen"). */
1116
+ {
1117
+ kind: "pets";
1118
+ };
1119
+ /** What the amenity preview shows. */
1120
+ interface AmenityPreviewOptions {
1121
+ /** Ranked highlights (count echoes skipped), or a cross-section of the categories
1122
+ * (⌈count / groups⌉ from each, in order). Default "highlights". */
1123
+ from?: "highlights" | "categories";
1124
+ /** Rows at most. Default 8; 0 → no rows. */
1125
+ count?: number;
1126
+ /** Facts that lead the rows when the rental states them. Default []. */
1127
+ lead?: readonly ("distance" | "pets")[];
1128
+ /** Kinds the leading distance may state, by priority. Default WATER_KINDS. */
1129
+ distanceKinds?: readonly DistanceKind[];
1130
+ }
1131
+ /** The preview rows and `total` — every item of the catalogue (what "all amenities" opens). */
1132
+ type AmenityPreview = (rental: RentalDetail, options?: AmenityPreviewOptions) => {
1133
+ items: AmenityPreviewItem[];
1134
+ total: number;
1135
+ };
1136
+ /** See {@link AmenityPreview}. */
1137
+ declare const amenityPreview: AmenityPreview;
1138
+ //#endregion
1139
+ //#region src/rental/rooms.d.ts
1140
+ /** Room attribute prefixes never shown: the floor covering ("Art des Bodens: Laminat") decides
1141
+ * nothing and spends a slot (a fleet decision). Prefixes, because the values are open-ended. */
1142
+ declare const HIDDEN_ROOM_ATTRIBUTES: readonly string[];
1143
+ //#endregion
1144
+ //#region src/rental/catalogue.d.ts
1145
+ /** German transliteration (ä→ae, ß→ss), other diacritics stripped, lowercase, hyphenated,
1146
+ * ≤ 60 characters. URL generation, not content. */
1147
+ type Slugify = (name: string) => string;
1148
+ /** Deterministic, collision-free slugs in id order (a colliding slug gets the first 6 id
1149
+ * characters, then the whole id); `overrides` (rental id → slug) win, e.g. a template's fixed
1150
+ * short slugs, and throw when two of them collide. Search results must link with this map. */
1151
+ type AssignSlugs = (rentals: readonly Pick<CatalogueRental, "id" | "nameOrLabel">[], options?: {
1152
+ readonly overrides?: Readonly<Record<string, string>>;
1153
+ }) => Map<string, string>;
1154
+ /** Which flags `fetchRentalFlags` answers, and in which language it searches. */
1155
+ interface FetchRentalFlagsOptions {
1156
+ /** The search locale. */
1157
+ locale: Locale;
1158
+ /** Flag → the dateless live-search key that answers it. Default `DEFAULT_FLAG_FILTERS`. */
1159
+ filters?: Partial<Record<RentalFlag, string>>;
1160
+ }
1161
+ /** The three flags every backend answers on most tenants (`pets`, `locBeach`, `locCity`). A
1162
+ * site adds keys its tenant supports (seaView → its filter key), never a distance stand-in. */
1163
+ declare const DEFAULT_FLAG_FILTERS: Readonly<Partial<Record<RentalFlag, string>>>;
1164
+ /** The flags per rental, and the flags the tenant cannot answer. */
1165
+ interface RentalFlagsResult {
1166
+ /** Per rental of the baseline: `true` matched, `false` did not; ignored flags are absent. */
1167
+ byRental: ReadonlyMap<string, Partial<Record<RentalFlag, boolean>>>;
1168
+ /** Flags the tenant cannot answer: the key came back in `unusedFilterKeys`, on v9 the match
1169
+ * equals the unfiltered baseline (the hub ignores an unknown key and returns everything — one
1170
+ * once marked all 41 rentals "at the beach"; v10 reports such a key in `unusedFilterKeys`, so a
1171
+ * flag every rental has stays true there), or the search failed (logged) or stopped before its
1172
+ * last page. No rental carries an ignored flag. */
1173
+ ignored: RentalFlag[];
1174
+ }
1175
+ /** BUILD TIME. One unfiltered baseline + one dateless `<key>=true` search per flag, sequential,
1176
+ * every page walked. Fails soft: a failed search ignores its flag (a failed baseline every
1177
+ * flag) and never fails the build. */
1178
+ type FetchRentalFlags = (sdk: WebsiteSdk, options: FetchRentalFlagsOptions) => Promise<RentalFlagsResult>;
1179
+ /** Labels on ≥ `universalShare` (default 0.9) of the portfolio move to the back, stable within
1180
+ * both halves: it reorders, it never drops. Portfolio-relative (no SDK option can state it).
1181
+ * Under ~20 rentals the share is a two-position switch — measure before moving it. */
1182
+ type RankHighlights = <T extends {
1183
+ readonly highlights?: readonly string[];
1184
+ }>(rentals: readonly T[], options?: {
1185
+ readonly universalShare?: number;
1186
+ }) => T[];
1187
+ /** What the build-time catalogue loader needs: the adapter's options plus the flag joins and slug overrides. */
1188
+ interface LoadRentalCatalogueOptions extends Omit<AdaptRentalOptions, "slug" | "flags" | "locale"> {
1189
+ locale: Locale;
1190
+ /** Flag joins (`FetchRentalFlagsOptions.filters`), or false to skip them. Default
1191
+ * `DEFAULT_FLAG_FILTERS`. */
1192
+ flags?: FetchRentalFlagsOptions["filters"] | false;
1193
+ slugOverrides?: Readonly<Record<string, string>>;
1194
+ universalShare?: number;
1195
+ }
1196
+ /** BUILD TIME. `getRentals` (one retry after 1.5 s: a cold start once answered a non-JSON
1197
+ * body) → flags → slugs → `adaptRental` → `rankHighlights`, sorted by name in `locale`. */
1198
+ type LoadRentalCatalogue = (sdk: WebsiteSdk, options: LoadRentalCatalogueOptions) => Promise<RentalDetail[]>;
1199
+ /** See {@link Slugify}. */
1200
+ declare const slugify: Slugify;
1201
+ /** See {@link AssignSlugs}. */
1202
+ declare const assignSlugs: AssignSlugs;
1203
+ /** See {@link FetchRentalFlags}. */
1204
+ declare const fetchRentalFlags: FetchRentalFlags;
1205
+ /** See {@link RankHighlights}. */
1206
+ declare const rankHighlights: RankHighlights;
1207
+ /** See {@link LoadRentalCatalogue}. */
1208
+ declare const loadRentalCatalogue: LoadRentalCatalogue;
1209
+ //#endregion
1210
+ //#region src/rental/text.d.ts
1211
+ /** How much of a description shows before the "more" control. */
1212
+ type PreviewStrategy =
1213
+ /** Whole blocks while they fit `budget` characters, then the crossing paragraph cut at its
1214
+ * last sentence end within the budget; a preview shorter than `min` takes the next sentence
1215
+ * end instead (never a stub). Ordinals ("im 1. Stock") and one-letter abbreviations ("z.B.",
1216
+ * "e.g.") are not sentence ends. Default budget 320, min 160 (measured: every preview
1217
+ * 169–342 characters). Lists are never cut. */
1218
+ {
1219
+ by: "sentences";
1220
+ budget?: number;
1221
+ min?: number;
1222
+ } |
1223
+ /** The first `count` blocks. */
1224
+ {
1225
+ by: "paragraphs";
1226
+ count: number;
1227
+ } |
1228
+ /** Nothing visible; everything behind the control. */
1229
+ {
1230
+ by: "none";
1231
+ };
1232
+ /** The visible part of a description and whether there is more. */
1233
+ interface DescriptionPreview {
1234
+ /** The visible blocks; a cut paragraph appears as its visible half. */
1235
+ shown: RichBlock[];
1236
+ /** Something is not shown whole. */
1237
+ hasMore: boolean;
1238
+ }
1239
+ /** The preview of `blocks` (default `{ by: "sentences" }`). */
1240
+ type DescriptionPreviewOf = (blocks: readonly RichBlock[], strategy?: PreviewStrategy) => DescriptionPreview;
1241
+ /** Plain text for a meta description: the first paragraph of the body (no heading, rule or
1242
+ * list) that is not a bold-only headline (most feed texts open with `**Name – Slogan**`, which
1243
+ * only repeats the title; the first paragraph when every one is), markup removed, cut at a word
1244
+ * boundary so the text and its " …" fit `max` (default 160; `plainText`). "" when there is none. */
1245
+ type DescriptionSummary = (content: DescriptionContent, options?: {
1246
+ readonly max?: number;
1247
+ }) => string;
1248
+ /** See {@link DescriptionPreviewOf}. */
1249
+ declare const descriptionPreview: DescriptionPreviewOf;
1250
+ /** See {@link DescriptionSummary}. */
1251
+ declare const descriptionSummary: DescriptionSummary;
1252
+ //#endregion
1253
+ //#region src/rental/reviews.d.ts
1254
+ /** Shorten a guest's name: every word but the last kept, the last cut to its initial
1255
+ * ("Max M.", "Erika Maria M.", "Anna-Lena P."); trailing titles skipped ("Max Probe
1256
+ * Dr." → "Max P."); a split double name joined ("Probe- Muster" is one surname); one word
1257
+ * unchanged (it cannot be told apart); the PMS placeholders ("Gastname nicht genannt") →
1258
+ * undefined (the UI says "Gast"). An already short name passes unchanged. */
1259
+ type ShortReviewerName = (name: string) => string | undefined;
1260
+ /** Split an SDK author string at the last comma whose tail ends in a year: the shortened name
1261
+ * and the stay as the SDK wrote it ("Anna Berg, verreist im Mai 2025" → { name: "Anna B.",
1262
+ * stay: "verreist im Mai 2025" }). Wording-agnostic, so de-DE and en-US split alike. */
1263
+ type SplitReviewAuthor = (author: string | undefined) => {
1264
+ name?: string;
1265
+ stay?: string;
1266
+ };
1267
+ /** Newest first by the stay month at the end of `author` (month names read through `Intl` in
1268
+ * `locale` — the locale `getRentals` ran with), `createdAt` within a month and for undated
1269
+ * items; stable. All or nothing: when any author carries a year whose month does not read in
1270
+ * `locale`, the feed's order is returned untouched (a half-sorted list looks deliberate). */
1271
+ type NewestReviewsFirst = <T extends {
1272
+ author?: string;
1273
+ createdAt?: string;
1274
+ }>(items: readonly T[], locale: Locale) => T[];
1275
+ /** See {@link ShortReviewerName}. */
1276
+ declare const shortReviewerName: ShortReviewerName;
1277
+ /** See {@link SplitReviewAuthor}. */
1278
+ declare const splitReviewAuthor: SplitReviewAuthor;
1279
+ /** See {@link NewestReviewsFirst}. */
1280
+ declare const newestReviewsFirst: NewestReviewsFirst;
1281
+ //#endregion
1282
+ //#region src/search/items.d.ts
1283
+ /** One rental in the result list. Strings verbatim from the SDK. */
1284
+ interface SearchResultItem {
1285
+ /** The rental id. */
1286
+ readonly id: string;
1287
+ /** facts.name or nameOrLabel, trimmed. */
1288
+ readonly name: string;
1289
+ /** facts.location or address.city; "" when neither (a fallback place asserts nothing). */
1290
+ readonly location: string;
1291
+ /** facts.href (build-time, collision-free) else `rentalHref` filled, + the handover query
1292
+ * (`handoverQuery(search, { stay, offerLabels })`). */
1293
+ readonly href: string;
1294
+ /** The same rental page with the party but no dates ("other dates": the guest picks). */
1295
+ readonly hrefWithoutDates: string;
1296
+ /** WebP-rewritten where the host serves it (until the SDK emits WebP itself). */
1297
+ readonly images: readonly CardImage[];
1298
+ /** facts first, then counts read from the highlights. */
1299
+ readonly counts: RentalCounts;
1300
+ /** The feed's highlights, whole (the card model removes spec echoes by shape). */
1301
+ readonly highlights: readonly string[];
1302
+ /** The SDK's translated rental type, else the facts'. */
1303
+ readonly rentalType: string | null;
1304
+ /** Null when the feed priced nothing (an undated search, an unpriced hit). */
1305
+ readonly price: StayPrice | null;
1306
+ /** The stay the price is for when it is not simply the searched dates. */
1307
+ readonly stay: StayWindow | null;
1308
+ /** Non-exact result (alternatives bucket); framed as "alternative" only on exact searches. */
1309
+ readonly alternative: boolean;
1310
+ /** Every window the backend suggested, best first; [] on exact matches. */
1311
+ readonly periods: readonly AlternativePeriod[];
1312
+ /** facts.propertyId wins (v9 never sends one, v10 unreliably), else the item's property. */
1313
+ readonly propertyId: string | null;
1314
+ /** The item's location, else facts.coords; null keeps it off the map. */
1315
+ readonly coords: {
1316
+ readonly lat: number;
1317
+ readonly lng: number;
1318
+ } | null;
1319
+ /** From the facts only (the search feed carries no reviews). */
1320
+ readonly rating: CardRating | null;
1321
+ }
1322
+ /** How result links are built and which build-time facts join the live items. */
1323
+ interface ResultLinkOptions {
1324
+ /** Template with {slug} (slugify(name)) or {id}, for rentals the facts give no href. Required:
1325
+ * routing is the site's knowledge, never implied by the locale. */
1326
+ readonly rentalHref: RouteTemplate;
1327
+ /** Build-time facts by rental id (they fill what the live projection lacks). */
1328
+ readonly facts?: RentalFactsMap;
1329
+ /** The language of the stay labels and of the PMS strings asked for. */
1330
+ readonly locale: Locale;
1331
+ }
1332
+ /** A catalogue rental as a result item for a server-rendered listing: the same fields the live
1333
+ * items take, no price, no stay, a link without booking context (there is no search to carry). */
1334
+ type CatalogueResultItem = (rental: RentalDetail, options: ResultLinkOptions) => SearchResultItem;
1335
+ /**
1336
+ * The order an undated search with no sort returns, as far as a build can reproduce it, so the
1337
+ * island's swap over a prerendered list moves nothing: numeric id ascending for v9 (all-digit
1338
+ * ids), UTF-16 code unit order for v10 (a plain `<`; `localeCompare` folds case and misorders
1339
+ * these ids). Measured, not a contract — the island keeps the server list's order on the bare URL.
1340
+ */
1341
+ type CompareUndatedSearchOrder = (a: {
1342
+ readonly id: string;
1343
+ }, b: {
1344
+ readonly id: string;
1345
+ }) => number;
1346
+ /** The card input for one result: live fields from the item, catalogue fields from the facts
1347
+ * (live wins). `nights` is the item's own stay length; the page-level `QueryShape.nights`
1348
+ * answers for exact matches of the searched dates. */
1349
+ type ResultCardSource = (item: SearchResultItem, facts?: RentalFacts) => CardSource;
1350
+ /** See {@link CatalogueResultItem}. */
1351
+ declare const catalogueResultItem: CatalogueResultItem;
1352
+ /** See {@link CompareUndatedSearchOrder}. */
1353
+ declare const compareUndatedSearchOrder: CompareUndatedSearchOrder;
1354
+ /** See {@link ResultCardSource}. */
1355
+ declare const resultCardSource: ResultCardSource;
1356
+ //#endregion
1357
+ //#region src/map/points.d.ts
1358
+ /** A coordinate pair in MapLibre order. */
1359
+ type LngLat = readonly [lng: number, lat: number];
1360
+ /** West-south and east-north corners. */
1361
+ type MapBounds = readonly [sw: LngLat, ne: LngLat];
1362
+ /** One result on the map. */
1363
+ interface MapPoint {
1364
+ /** The rental id (the card carries the same in `data-result-card`). */
1365
+ readonly id: string;
1366
+ /** The result's coordinates. */
1367
+ readonly at: LngLat;
1368
+ /** Pin text; null = dot. */
1369
+ readonly label: string | null;
1370
+ /** The total as a number, for ordering only (cheapest first in a same-spot circle); never shown. */
1371
+ readonly sortValue: number | null;
1372
+ }
1373
+ /** How a pin states the price: the same decision the cards take. */
1374
+ interface PinLabelOptions {
1375
+ /** "hidden": every pin is a dot (a no-price ruling). Default "total". */
1376
+ readonly price?: "total" | "hidden";
1377
+ /** Default "hide-zero" (the cards' default). */
1378
+ readonly cents?: PriceCents;
1379
+ }
1380
+ /** Mappable results in result order; items without coords skipped (never a guessed pin). */
1381
+ type MapPoints = (items: readonly SearchResultItem[], options?: PinLabelOptions) => MapPoint[];
1382
+ /** At least one result can be pinned (the map's and the mobile map pill's condition). */
1383
+ type HasMapPoints = (items: readonly {
1384
+ readonly coords: unknown;
1385
+ }[]) => boolean;
1386
+ /** See {@link MapPoints}. */
1387
+ declare const mapPoints: MapPoints;
1388
+ /** See {@link HasMapPoints}. */
1389
+ declare const hasMapPoints: HasMapPoints;
1390
+ //#endregion
1391
+ //#region src/map/same-spot.d.ts
1392
+ /** "fan" (default): 2–5 pins cascade like a fanned deck (13 px / 9 px steps); 6 or more sit on a
1393
+ * circle, so every pin keeps its own position (a capped cascade stacked four of ten pins on one
1394
+ * offset: "of ten clicks the preview opens three times"). "group": 3 or more collapse into one
1395
+ * count bubble that fans out on a circle when clicked; pairs stay two offset pins (bubbling pairs
1396
+ * hid prices to solve few stacks). For single-building portfolios. */
1397
+ type SameSpotMode = "fan" | "group";
1398
+ /** A pixel offset `[x, y]` (y down). */
1399
+ type PixelOffset = readonly [x: number, y: number];
1400
+ /** A collapsed spot in "group" mode. */
1401
+ interface SameSpotGroup {
1402
+ /** Stable while the member set is unchanged. */
1403
+ readonly key: string;
1404
+ /** The spot's first pin's coordinates. */
1405
+ readonly at: LngLat;
1406
+ /** Cheapest first (by `sortValue`), then result order. */
1407
+ readonly ids: readonly string[];
1408
+ }
1409
+ /** Where each pin sits. */
1410
+ interface SameSpotLayout {
1411
+ /** Offset per rental id for pins drawn individually. Absent = true position. */
1412
+ readonly offsets: Readonly<Record<string, PixelOffset>>;
1413
+ /** "group" mode only; empty in "fan" mode. */
1414
+ readonly groups: readonly SameSpotGroup[];
1415
+ }
1416
+ /** The mode and the measured pin width. */
1417
+ interface SameSpotOptions {
1418
+ /** "fan" or "group" (see `SameSpotMode`). */
1419
+ readonly mode: SameSpotMode;
1420
+ /** Measured width of the widest pin body in px (a dot is 20). Sizes the circles. */
1421
+ readonly pinWidthPx: number;
1422
+ }
1423
+ /** Haversine distance in metres. */
1424
+ type DistanceMeters = (a: LngLat, b: LngLat) => number;
1425
+ /** Offsets of `count` pins on a circle that does not let them touch: radius = max(38,
1426
+ * (pinWidthPx + 12) · count / 2π), first pin at 12 o'clock, clockwise. */
1427
+ type CircleOffsets = (count: number, pinWidthPx: number) => PixelOffset[];
1428
+ /** Group by real distance and lay each spot out. Pure; same input, same output. */
1429
+ type LayoutSameSpot = (points: readonly MapPoint[], options: SameSpotOptions) => SameSpotLayout;
1430
+ /** See {@link DistanceMeters}. */
1431
+ declare const distanceMeters: DistanceMeters;
1432
+ /** See {@link CircleOffsets}. */
1433
+ declare const circleOffsets: CircleOffsets;
1434
+ /** See {@link LayoutSameSpot}. */
1435
+ declare const layoutSameSpot: LayoutSameSpot;
1436
+ //#endregion
1437
+ //#region src/map/frame.d.ts
1438
+ /** How the camera fits the pins. */
1439
+ interface FitOptions {
1440
+ /** px around the fitted pins. Default 56. */
1441
+ readonly padding?: number;
1442
+ /** Never zoom closer than this when fitting (a lone pin). Default 12. */
1443
+ readonly maxZoom?: number;
1444
+ /** Leave far-away pins out of the FIRST FRAME (they stay on the map): a point farther than 10×
1445
+ * the median distance to the median centre, from 4 points up. Default true. */
1446
+ readonly excludeOutliers?: boolean;
1447
+ }
1448
+ /** The default framing (padding 56, maxZoom 12, outliers excluded). */
1449
+ type DefaultFit = typeof DEFAULT_FIT;
1450
+ /** The bounds the camera fits, or null when there is nothing to frame. */
1451
+ type FrameFor = (points: readonly MapPoint[], options?: FitOptions) => MapBounds | null;
1452
+ /** Ids of the points inside `bounds` (edges included), in point order. */
1453
+ type IdsInView = (points: readonly MapPoint[], bounds: MapBounds) => string[];
1454
+ declare const DEFAULT_FIT: Required<FitOptions>;
1455
+ /** See {@link FrameFor}. */
1456
+ declare const frameFor: FrameFor;
1457
+ /** See {@link IdsInView}. */
1458
+ declare const idsInView: IdsInView;
1459
+ //#endregion
1460
+ //#region src/store/store.d.ts
1461
+ /** Receives the new state after every change. */
1462
+ type Listener<S> = (state: S) => void;
1463
+ /** Stops a listener from receiving further states. Calling it twice is harmless. */
1464
+ type Unsubscribe = () => void;
1465
+ /** Read-only view every store exposes. */
1466
+ interface Store<S> {
1467
+ /** The current state; the same reference until the next change. */
1468
+ getState(): S;
1469
+ /** Called with each new state; not called for the state that is current at subscription. */
1470
+ subscribe(listener: Listener<S>): Unsubscribe;
1471
+ }
1472
+ /** A store its owner can write; `setState` with the same reference is a no-op. */
1473
+ interface WritableStore<S> extends Store<S> {
1474
+ /** Replace the state (or derive it from the previous one) and notify every listener. */
1475
+ setState(next: S | ((prev: S) => S)): void;
1476
+ }
1477
+ /** A store holding `initial` until its owner writes. `S` is never a function itself: `setState` reads a
1478
+ * function argument as the updater form. */
1479
+ type CreateStore = <S>(initial: S) => WritableStore<S>;
1480
+ /** See {@link CreateStore}. */
1481
+ declare const createStore: CreateStore;
1482
+ //#endregion
1483
+ //#region src/guests/party.d.ts
1484
+ /** The travelling party. `adults` 0 means none chosen yet (a search sends the
1485
+ * default); `childrenAges[i]` is null while child i has no age. */
1486
+ interface Party {
1487
+ /** Adults (from 18); 0 = none chosen. */
1488
+ adults: number;
1489
+ /** Children inside the child-age band. */
1490
+ children: number;
1491
+ /** One entry per child, in order; null = not chosen yet. */
1492
+ childrenAges: readonly (number | null)[];
1493
+ /** Children younger than the child-age band. */
1494
+ babies: number;
1495
+ /** Dogs. */
1496
+ pets: number;
1497
+ }
1498
+ /** The four party types a stepper edits. */
1499
+ type GuestKind = "adults" | "children" | "babies" | "pets";
1500
+ /** Stepper maxima; 0 = the type is not offered; pets 1 = a yes/no choice. */
1501
+ interface GuestLimits {
1502
+ /** Most adults. */
1503
+ adults: number;
1504
+ /** Most children. */
1505
+ children: number;
1506
+ /** Most babies. */
1507
+ babies: number;
1508
+ /** Most dogs. */
1509
+ pets: number;
1510
+ }
1511
+ /** The kit limits every site ran on; a site overrides single keys. */
1512
+ declare const DEFAULT_GUEST_LIMITS: Readonly<GuestLimits>;
1513
+ /** Selectable child ages; younger is a baby. */
1514
+ interface ChildAgeRange {
1515
+ /** Youngest child age (a younger child is a baby). */
1516
+ min: number;
1517
+ /** Oldest child age (older travels as an adult). */
1518
+ max: number;
1519
+ }
1520
+ /** The kit's child-age band. */
1521
+ declare const DEFAULT_CHILD_AGE: Readonly<ChildAgeRange>;
1522
+ /** A rental's caps (booking contexts). Precedence: availability (maxPersons/maxAdults) > catalogue maxGuests
1523
+ * (used only where availability states none: v9) > limits. A stated cap above the limit wins (a 20-person house). */
1524
+ interface GuestCaps {
1525
+ /** From availability: binds adults + children. */
1526
+ readonly maxPersons?: number | null;
1527
+ /** From availability: binds adults. */
1528
+ readonly maxAdults?: number | null;
1529
+ /** The catalogue's sleeping capacity; binds adults + children only where `maxPersons` states none (v9). */
1530
+ readonly maxGuests?: number | null;
1531
+ /** From availability (v9): false when the rental takes no children, babies included. */
1532
+ readonly childrenAllowed?: boolean | null;
1533
+ /** From availability (v9): the most dogs the quote accepts, 0 for none; null states no limit. */
1534
+ readonly maxPets?: number | null;
1535
+ /** false: no dog can be ADDED; a count that arrived (URL) can be lowered, never raised, and is still sent —
1536
+ * the flag is unreliable on several feeds, so the guest's declared party is never changed silently. */
1537
+ readonly petsAllowed?: boolean;
1538
+ }
1539
+ /** The per-surface guest options (SearchBar `guests`, Quote `guests`, Booking `guests`). */
1540
+ interface GuestOptions {
1541
+ /** Overrides single limits of `DEFAULT_GUEST_LIMITS`. */
1542
+ readonly limits?: Partial<GuestLimits>;
1543
+ /** Default `DEFAULT_CHILD_AGE`. */
1544
+ readonly childAge?: ChildAgeRange;
1545
+ /** Adults when none is chosen / the URL states none. Default 2 (one site: 1). */
1546
+ readonly defaultAdults?: number;
1547
+ }
1548
+ /** What one stepper may do. */
1549
+ interface GuestBound {
1550
+ /** false: render no control for this type. */
1551
+ readonly offered: boolean;
1552
+ /** Lowest count (adults 1, everything else 0). */
1553
+ readonly min: number;
1554
+ /** Highest count. */
1555
+ readonly max: number;
1556
+ }
1557
+ /** One bound per party type. */
1558
+ type GuestBounds = Readonly<Record<GuestKind, GuestBound>>;
1559
+ /** Bounds for `party` (pass it as it will be sent: adults resolved). Adults win a shared person cap: children
1560
+ * get what the adults leave, so an overfull handover drops children, never adults. Clamp an edit against the
1561
+ * bounds of the party BEFORE the edit: without the pets flag the dogs that arrived are the ceiling. */
1562
+ type GuestBoundsFor = (party: Party, input: {
1563
+ readonly limits?: Partial<GuestLimits>;
1564
+ readonly caps?: GuestCaps;
1565
+ }) => GuestBounds;
1566
+ /** Clamp offered types to the bounds (pets per the GuestCaps rule); a not-offered type keeps its value, and
1567
+ * adults 0 (none chosen) stays 0. Ages follow the children count (trimmed, or padded with null). */
1568
+ type ClampParty = (party: Party, bounds: GuestBounds) => Party;
1569
+ /** Every child has an age (the age prices the stay). */
1570
+ type AgesComplete = (party: Party) => boolean;
1571
+ /** Dates + party as one comparable string (quote/checkout supersession). */
1572
+ type StayKeyOf = (stay: {
1573
+ readonly start: IsoDay;
1574
+ readonly end: IsoDay;
1575
+ readonly party: Party;
1576
+ }) => string;
1577
+ /** The occupancy the SDK's quote and search take (0.1.x `QuoteOccupancy`, moved here). */
1578
+ interface QuoteOccupancy {
1579
+ /** Adults. */
1580
+ adults: number;
1581
+ /** Children. */
1582
+ children: number;
1583
+ /** The chosen ages only; absent when none is chosen. */
1584
+ childrenAges?: number[] | undefined;
1585
+ /** Babies. */
1586
+ babies: number;
1587
+ /** Dogs. */
1588
+ pets: number;
1589
+ }
1590
+ /** SDK occupancy; unchosen ages left out (the flows block on them). */
1591
+ type OccupancyOf = (party: Party) => QuoteOccupancy;
1592
+ /** See {@link GuestBoundsFor}. */
1593
+ declare const guestBounds: GuestBoundsFor;
1594
+ /** See {@link ClampParty}. */
1595
+ declare const clampParty: ClampParty;
1596
+ /** See {@link AgesComplete}. */
1597
+ declare const agesComplete: AgesComplete;
1598
+ /** See {@link StayKeyOf}. */
1599
+ declare const stayKeyOf: StayKeyOf;
1600
+ /** See {@link OccupancyOf}. */
1601
+ declare const occupancyOf: OccupancyOf;
1602
+ //#endregion
1603
+ //#region src/url/params.d.ts
1604
+ /** Every key of the funnel's wire format. Public: links live in bookmarks, indexes and mails. Renaming one
1605
+ * is a major. */
1606
+ interface SearchParamKeys {
1607
+ /** Arrival (YYYY-MM-DD); on a ± or night-range search the window's first day. */
1608
+ readonly start: "start";
1609
+ /** Departure (YYYY-MM-DD); on a ± or night-range search the window's last day. */
1610
+ readonly end: "end";
1611
+ /** v10 ± window, `DD-MM-YYYY,DD-MM-YYYY`. */
1612
+ readonly dates: "dates";
1613
+ /** Stay length beside `dates` or `month`. */
1614
+ readonly nights: "nights";
1615
+ /** A flexible month, `MM-YYYY`, repeatable (v10). */
1616
+ readonly month: "month";
1617
+ /** The weekend flag beside `month` (written `weekend=1`, sent bare). */
1618
+ readonly weekend: "weekend";
1619
+ /** Shortest stay inside a start/end window. */
1620
+ readonly minNights: "minNights";
1621
+ /** Longest stay inside a start/end window. */
1622
+ readonly maxNights: "maxNights";
1623
+ /** The ± step the guest chose; UI-only, never sent to the SDK (restores the stay losslessly). */
1624
+ readonly flex: "flex";
1625
+ /** Adults. */
1626
+ readonly adults: "adults";
1627
+ /** Children. */
1628
+ readonly children: "children";
1629
+ /** Child ages, one comma list aligned to `children` (an unchosen age is empty). */
1630
+ readonly childrenAges: "childrenAges";
1631
+ /** Babies. */
1632
+ readonly babies: "babies";
1633
+ /** Search pages write the dog count as petsCount … */
1634
+ readonly petsCount: "petsCount";
1635
+ /** … rental and checkout pages as pets (an integer); legacy search links carry it as a flag. */
1636
+ readonly pets: "pets";
1637
+ /** A region scope, repeatable, OR-combined, applied in the browser. */
1638
+ readonly region: "region";
1639
+ /** A property (Anlage) scope. */
1640
+ readonly property: "property";
1641
+ /** The results ordering. */
1642
+ readonly sort: "sort";
1643
+ /** An offer campaign scope. */
1644
+ readonly campaign: "campaign";
1645
+ /** The cancellation rate chosen on the rental page (rental → checkout). */
1646
+ readonly policy: "policy";
1647
+ /** A booking kind handed to the checkout. */
1648
+ readonly kind: "kind";
1649
+ /** The payment-return marker on the checkout page. */
1650
+ readonly return: "return";
1651
+ /** An offer label carried from a result to the rental page, repeatable. */
1652
+ readonly offer: "offer";
1653
+ }
1654
+ /** How long a flexible stay is: the backend's weekend flag, seven nights, or the days of the earliest month. */
1655
+ type FlexibleStay = "weekend" | "week" | "month";
1656
+ /** A search period by meaning; `writeSearch` spells it per backend. */
1657
+ type SearchPeriod =
1658
+ /** Arrive `start`, leave `end`. URL: start + end. */
1659
+ {
1660
+ readonly kind: "exact";
1661
+ readonly start: IsoDay;
1662
+ readonly end: IsoDay;
1663
+ } |
1664
+ /** The guest's own stay (`start`–`end`), widened by `days` on each side, its length held.
1665
+ * v10: dates=DD-MM-YYYY,DD-MM-YYYY + nights + flex. v9: widened start/end + minNights = maxNights + flex.
1666
+ * Lower bound clamped to the floor, upper never (lossless restore from end − flex). */
1667
+ {
1668
+ readonly kind: "plusMinus";
1669
+ readonly start: IsoDay;
1670
+ readonly end: IsoDay;
1671
+ readonly days: number;
1672
+ } |
1673
+ /** Any stay of minNights..maxNights nights inside the window start–end. URL: start + end + the pair. */
1674
+ {
1675
+ readonly kind: "nightRange";
1676
+ readonly start: IsoDay;
1677
+ readonly end: IsoDay;
1678
+ readonly minNights: number;
1679
+ readonly maxNights: number;
1680
+ } |
1681
+ /** v10 only: repeated month=MM-YYYY + weekend=1 | nights=7 | nights=<days of the earliest month>. */
1682
+ {
1683
+ readonly kind: "flexible";
1684
+ readonly months: readonly IsoMonth[];
1685
+ readonly stay: FlexibleStay;
1686
+ };
1687
+ /** One `[key, value]` pair of a query string, in order. */
1688
+ type QueryPair = readonly [key: string, value: string];
1689
+ /** A search URL read into meaning. */
1690
+ interface SearchQuery {
1691
+ /** null: no period this backend can run (a stale month/dates link on v9 reads as null). */
1692
+ readonly period: SearchPeriod | null;
1693
+ /** null: no party key at all. adults 0 = none chosen. */
1694
+ readonly party: Party | null;
1695
+ /** `region` values: trimmed, de-duplicated, in URL order. */
1696
+ readonly regions: readonly string[];
1697
+ /** Every other pair in URL order (filters, property, sort, campaign, site keys). Never: owned keys, flex, the
1698
+ * legacy pets flag, alias keys, tracking parameters, empty values. */
1699
+ readonly carried: readonly QueryPair[];
1700
+ }
1701
+ /** Legacy keys read as a canonical one, never written; a repeatable alias value splits on commas. */
1702
+ type SearchParamAliases = Partial<Record<"start" | "end" | "region" | "adults" | "children" | "childrenAges" | "babies" | "petsCount", readonly string[]>>;
1703
+ /** The site's search contract: one `SEARCH_CONTRACT` constant, the `contract` prop of SearchBar,
1704
+ * SearchResults and OfferCampaignFilter (and the stores they create). */
1705
+ interface SearchContract {
1706
+ /** Which PMS answers. Decides how a period is spelled, through the SDK's capability helpers. */
1707
+ readonly backend: BackendKind;
1708
+ /** Whether this backend PRICES a min/maxNights window. Default true (measured on several v9 hubs); a hub
1709
+ * that answers unpriced sets false: no control writes the pair, readSearch ignores it, toSdkQuery strips it. */
1710
+ readonly nightRangeSearch?: boolean;
1711
+ /** Always read without configuration: from→start, till→end, nights_min/nights_max, legacy pets flag. */
1712
+ readonly aliases?: SearchParamAliases;
1713
+ }
1714
+ /** Options for writing a query. */
1715
+ interface WriteSearchOptions {
1716
+ /** First day a ± window may reach back to. Default today. */
1717
+ readonly floor?: IsoDay;
1718
+ /** Adults sent when none is chosen. Default 2 (a dated v10 search without occupancy answers 200 null). */
1719
+ readonly defaultAdults?: number;
1720
+ }
1721
+ /** URL query string (with or without `?`) → meaning. Tolerant: anything unreadable degrades to "not stated",
1722
+ * never to an error. `today` (default: the local date) drops flexible months already past. */
1723
+ type ReadSearch = (search: string, contract: SearchContract, today?: IsoDay) => SearchQuery;
1724
+ /** Meaning → canonical pairs, spelled for `contract.backend`, in a stable order (period, party, regions,
1725
+ * carried). Dogs as `petsCount` alone; a period the backend cannot spell is omitted; no empty values. A
1726
+ * written period always travels with adults (the chosen ones, else `defaultAdults`). */
1727
+ type WriteSearch = (query: SearchQuery, contract: SearchContract, options?: WriteSearchOptions) => QueryPair[];
1728
+ /** action (may carry a #fragment) + writeSearch. */
1729
+ type SearchHref = (action: string, query: SearchQuery, contract: SearchContract, options?: WriteSearchOptions) => string;
1730
+ /** See {@link SearchParamKeys}. */
1731
+ declare const SEARCH_PARAMS: SearchParamKeys;
1732
+ /** See {@link ReadSearch}. */
1733
+ declare const readSearch: ReadSearch;
1734
+ /** See {@link WriteSearch}. */
1735
+ declare const writeSearch: WriteSearch;
1736
+ /** See {@link SearchHref}. */
1737
+ declare const searchHref: SearchHref;
1738
+ //#endregion
1739
+ //#region src/url/party.d.ts
1740
+ /** The party from any funnel URL: petsCount wins over pets; pets=true → 1; childrenAges aligned to children (missing → null). */
1741
+ type ReadParty = (params: URLSearchParams, aliases?: SearchParamAliases) => Party | null;
1742
+ /** Party pairs in canonical order; dogs under `petsKey` (search pages: "petsCount"; rental/checkout: "pets");
1743
+ * zero counts omitted; childrenAges only with children (null ages written empty so the order survives). */
1744
+ type PartyPairs = (party: Party, petsKey: "petsCount" | "pets") => QueryPair[];
1745
+ /** See {@link ReadParty}. */
1746
+ declare const readParty: ReadParty;
1747
+ /** See {@link PartyPairs}. */
1748
+ declare const partyPairs: PartyPairs;
1749
+ //#endregion
1750
+ //#region src/search/filters.d.ts
1751
+ /** What a filter (build-time measurement) or a facet option (live count) that matches nothing
1752
+ * does: "drop" it, "disable" it (kept, not clickable), or "show" it (kept clickable). */
1753
+ type WhenEmpty = "drop" | "disable" | "show";
1754
+ /** The site's curation: which filters the results offer, grouped and ordered for this portfolio. */
1755
+ interface FilterSpec {
1756
+ /** The panel groups, in panel order. */
1757
+ readonly groups: readonly FilterGroupSpec[];
1758
+ /** Keys the backend still executes but that match nothing here; stripped from every query (a
1759
+ * stale link must not empty the page invisibly). */
1760
+ readonly retired?: readonly string[];
1761
+ }
1762
+ /** One titled group of the filter panel. */
1763
+ interface FilterGroupSpec {
1764
+ /** Stable id (titles are copy and change with the language). */
1765
+ readonly id: string;
1766
+ /** The group heading, in the page language. */
1767
+ readonly title: string;
1768
+ /** The filters of the group, in panel order. */
1769
+ readonly entries: readonly FilterEntry[];
1770
+ /** No control, but its values are read, kept on apply, chipped and honoured. */
1771
+ readonly hidden?: boolean;
1772
+ }
1773
+ /** One filter; where it is answered decides the kind. Every entry may be `hidden`. */
1774
+ type FilterEntry =
1775
+ /** A `getFilters` key the backend executes (boolean, or an int minimum); label/max override. */
1776
+ {
1777
+ /** Answered by the backend. */
1778
+ readonly kind: "sdk";
1779
+ /** The `getFilters` query key (also the URL key). */
1780
+ readonly key: string;
1781
+ /** Overrides the SDK's label. */
1782
+ readonly label?: string;
1783
+ /** An int filter's stepper maximum (v9 often states none). */
1784
+ readonly max?: number;
1785
+ /** No control; the URL value is still read, chipped and honoured. */
1786
+ readonly hidden?: boolean;
1787
+ } |
1788
+ /** Boolean answered in the browser from facts.flags[key] (a key the backend lacks or that is
1789
+ * dead at this hub). */
1790
+ {
1791
+ /** Answered in the browser from the facts. */
1792
+ readonly kind: "flag";
1793
+ /** The URL key and the `facts.flags` key. */
1794
+ readonly key: string;
1795
+ /** The control label. */
1796
+ readonly label: string;
1797
+ /** No control; still honoured. */
1798
+ readonly hidden?: boolean;
1799
+ } |
1800
+ /** Int lower bound from facts.counts[field], else facts.numbers[field] (a free numeric fact). */
1801
+ {
1802
+ /** Answered in the browser from the facts. */
1803
+ readonly kind: "minimum";
1804
+ /** The URL key. */
1805
+ readonly key: string;
1806
+ /** The control label. */
1807
+ readonly label: string;
1808
+ /** The `facts.counts` / `facts.numbers` field compared. */
1809
+ readonly field: string;
1810
+ /** The stepper minimum. Default 1. */
1811
+ readonly min?: number;
1812
+ /** The stepper maximum. */
1813
+ readonly max: number;
1814
+ /** No control; still honoured. */
1815
+ readonly hidden?: boolean;
1816
+ } |
1817
+ /** OR within the facet, from facts.facets[key] (region, property, room type, floor …). */
1818
+ {
1819
+ /** Answered in the browser from the facts. */
1820
+ readonly kind: "facet";
1821
+ /** The URL key (repeatable) and the `facts.facets` key. */
1822
+ readonly key: string;
1823
+ /** The facet label. */
1824
+ readonly label: string;
1825
+ /** The values offered (`facetOptions` builds them from the facts). */
1826
+ readonly options: readonly FacetOption[];
1827
+ /** Several values at once (OR). Default true. */
1828
+ readonly multiple?: boolean;
1829
+ /** No control; still honoured. */
1830
+ readonly hidden?: boolean;
1831
+ } |
1832
+ /** The dogs pill (occupancy `petsCount`), placed exactly where the entry sits. */
1833
+ {
1834
+ readonly kind: "pets";
1835
+ readonly hidden?: boolean;
1836
+ };
1837
+ /** One value of a facet; `count` = rentals carrying it (build). */
1838
+ interface FacetOption {
1839
+ /** The URL value. */
1840
+ readonly value: string;
1841
+ /** The display label. */
1842
+ readonly label: string;
1843
+ /** Rentals carrying the value (build time). */
1844
+ readonly count?: number;
1845
+ }
1846
+ /** The minimal shape read off `sdk.static.filter.getFilters`; its output passes as it is. */
1847
+ interface SdkFilterDefinition {
1848
+ /** The URL / query key. */
1849
+ readonly searchParameterQueryKey: string;
1850
+ /** The SDK's translated label. */
1851
+ readonly label: string;
1852
+ /** The filter kind, every kind `getFilters` returns ("BooleanFilter", "IntFilter", "OptionFilter",
1853
+ * "StringFilter"). Only boolean and int filters become sdk entries; every other kind is skipped. */
1854
+ readonly type: FilterGetFiltersOutput[number]["type"];
1855
+ /** An int filter's bounds, when the backend states them. */
1856
+ readonly numericBounds?: {
1857
+ readonly min?: number;
1858
+ readonly max?: number;
1859
+ } | null;
1860
+ }
1861
+ /** A filter as the store and every control use it. */
1862
+ type FilterDef = {
1863
+ /** On/off. */
1864
+ readonly kind: "boolean";
1865
+ /** The URL key. */
1866
+ readonly key: string;
1867
+ /** The control label. */
1868
+ readonly label: string;
1869
+ /** Who answers it: the backend, or the browser from the facts. */
1870
+ readonly source: "sdk" | "facts";
1871
+ /** No control (still read, chipped and honoured). */
1872
+ readonly hidden: boolean;
1873
+ /** Measured to match nothing and kept (`whenEmpty: "disable"`): shown, not clickable. */
1874
+ readonly disabled: boolean;
1875
+ } | {
1876
+ /** An int lower bound. */
1877
+ readonly kind: "minimum";
1878
+ /** The URL key. */
1879
+ readonly key: string;
1880
+ /** The control label. */
1881
+ readonly label: string;
1882
+ /** Who answers it. */
1883
+ readonly source: "sdk" | "facts";
1884
+ /** The facts field compared (facts source only). */
1885
+ readonly field: string | null;
1886
+ /** Stepper minimum. */
1887
+ readonly min: number;
1888
+ /** Stepper maximum. */
1889
+ readonly max: number;
1890
+ /** No control. */
1891
+ readonly hidden: boolean;
1892
+ } | {
1893
+ /** OR within, AND across facets. */
1894
+ readonly kind: "facet";
1895
+ /** The URL key (repeatable). */
1896
+ readonly key: string;
1897
+ /** The facet label. */
1898
+ readonly label: string;
1899
+ /** The values offered. */
1900
+ readonly options: readonly FacetOption[];
1901
+ /** Several values at once. */
1902
+ readonly multiple: boolean;
1903
+ /** No control. */
1904
+ readonly hidden: boolean;
1905
+ } | {
1906
+ readonly kind: "pets";
1907
+ readonly hidden: boolean;
1908
+ };
1909
+ /** The curation resolved against the SDK. Serializable; computed at build, passed as a prop. */
1910
+ interface ResolvedFilters {
1911
+ /** The visible panel, groups in order (an empty group is dropped). */
1912
+ readonly groups: ReadonlyArray<{
1913
+ readonly id: string;
1914
+ readonly title: string;
1915
+ readonly hidden: boolean;
1916
+ readonly defs: readonly FilterDef[];
1917
+ }>;
1918
+ /** Every def, flat, hidden ones included (chips, round trip, matching). */
1919
+ readonly defs: readonly FilterDef[];
1920
+ /** Keys forwarded to the SDK. */
1921
+ readonly sdkKeys: readonly string[];
1922
+ /** Keys stripped from every SDK query (`FilterSpec.retired`). */
1923
+ readonly retired: readonly string[];
1924
+ }
1925
+ /** Build-time measurement (`measureFilters`) applied while resolving. */
1926
+ interface ResolveFiltersOptions {
1927
+ /** Hits per key against the baseline. */
1928
+ readonly counts?: Readonly<Record<string, number>>;
1929
+ /** The baseline total the counts were measured against. */
1930
+ readonly total?: number;
1931
+ /** A filter matching nothing (measured). Default "drop" (an honest filter never strands the
1932
+ * guest on zero). */
1933
+ readonly whenEmpty?: WhenEmpty;
1934
+ /** A filter matching everything does not filter (or the hub ignores it). Default "drop". */
1935
+ readonly whenUniversal?: "drop" | "keep";
1936
+ }
1937
+ /** The working selection of the filter panel. */
1938
+ interface FilterSelection {
1939
+ /** Boolean keys switched on. */
1940
+ readonly booleans: ReadonlySet<string>;
1941
+ /** Minimum per key. */
1942
+ readonly minimums: Readonly<Record<string, number>>;
1943
+ /** Selected values per facet key. */
1944
+ readonly facets: Readonly<Record<string, readonly string[]>>;
1945
+ /** Dog count; 0 = off. */
1946
+ readonly pets: number;
1947
+ }
1948
+ /** A scope the URL carries that the search store honours without a curated facet: `?region=` (the
1949
+ * search bar's region field) and `?property=` (an Anlage link). */
1950
+ type ImplicitScope = "region" | "property";
1951
+ /** One applied filter, as data; the component words it from its copy table. */
1952
+ interface ActiveFilter {
1953
+ /** Stable id: `key`, or `key=value` for one facet value. */
1954
+ readonly id: string;
1955
+ /** Which kind of def it came from (an implicit scope is a facet). */
1956
+ readonly kind: "boolean" | "minimum" | "facet" | "pets";
1957
+ /** The URL key (`petsCount` for dogs). */
1958
+ readonly key: string;
1959
+ /** The filter's label ("" for dogs and implicit scopes: the copy words them). */
1960
+ readonly label: string;
1961
+ /** Facet: the option's display label; an implicit scope's URL value verbatim (a region value, a
1962
+ * property id). */
1963
+ readonly value?: string;
1964
+ /** Minimum or dog count. */
1965
+ readonly count?: number;
1966
+ /** Set on an implicit scope's chip (the search store's `activeFilters` only): which scope, so the
1967
+ * copy can word it ("Region: {value}", "Anlage: {value}"). A curated facet for the key replaces
1968
+ * it, with its label and its option labels (a property's name instead of its id). */
1969
+ readonly scope?: ImplicitScope;
1970
+ }
1971
+ /** How `facetOptions` orders its options. */
1972
+ interface FacetOptionsOptions {
1973
+ /** Value → display label. Default: the value. */
1974
+ readonly labels?: Readonly<Record<string, string>>;
1975
+ /** "label" (default, locale-aware), "count-desc", or an explicit value list (unlisted values
1976
+ * follow by label). */
1977
+ readonly order?: "label" | "count-desc" | readonly string[];
1978
+ /** The collation of "label" order. Default "de-DE". */
1979
+ readonly locale?: Locale;
1980
+ }
1981
+ /** What `measureFilters` returns. */
1982
+ interface FilterMeasurement {
1983
+ /** Hits of the baseline query. */
1984
+ readonly total: number;
1985
+ /** Hits per key (a key whose count the backend does not state is absent). */
1986
+ readonly counts: Readonly<Record<string, number>>;
1987
+ }
1988
+ /** Resolve the curation: an sdk key the SDK no longer returns self-hides, Option and String
1989
+ * filters in an sdk entry are skipped, int bounds default to 1..8, measured empties and
1990
+ * universals follow `whenEmpty`/`whenUniversal`, an empty group is dropped; hidden groups hide
1991
+ * their defs. Pure. */
1992
+ type ResolveFilters = (sdkFilters: readonly SdkFilterDefinition[], spec: FilterSpec, options?: ResolveFiltersOptions) => ResolvedFilters;
1993
+ /** Read the URL into a selection over every def (hidden ones included). */
1994
+ type ReadSelection = (search: string, filters: ResolvedFilters) => FilterSelection;
1995
+ /** Write a selection: every visible def's key is rewritten from the selection; hidden defs keep
1996
+ * their URL values; dogs as `petsCount`, never downgraded; the legacy `pets` key is deleted;
1997
+ * every other key stays. Returns the query without "?". */
1998
+ type WriteSelection = (search: string, filters: ResolvedFilters, selection: FilterSelection) => string;
1999
+ /** A minimum's stepper moves from `value` (0 = any): the first step up lands on the filter's floor
2000
+ * (`max(def.min, 1)`) and the step down from the floor returns to "any", so no value below the floor
2001
+ * is ever offered; `up` stops at `def.max`, `down` at 0. */
2002
+ type MinimumSteps = (def: Extract<FilterDef, {
2003
+ kind: "minimum";
2004
+ }>, value: number) => {
2005
+ readonly down: number;
2006
+ readonly up: number;
2007
+ };
2008
+ /** The applied filters in def order (facets per value, hidden ones included), for chips. */
2009
+ type ActiveFilters = (search: string, filters: ResolvedFilters) => ActiveFilter[];
2010
+ /** The query without one applied filter (one facet value, or the whole key; dogs remove both
2011
+ * dog keys). */
2012
+ type WithoutFilter = (search: string, filter: ActiveFilter) => string;
2013
+ /** The query without every filter, hidden ones included (dates, party and scopes kept). */
2014
+ type WithoutFilters = (search: string, filters: ResolvedFilters) => string;
2015
+ /** The quick strip's defs in `keys` order: visible booleans only (a one-tap chip cannot state a
2016
+ * minimum); keys missing from `filters` drop out. */
2017
+ type QuickFilterDefs = (filters: ResolvedFilters, keys: readonly string[]) => FilterDef[];
2018
+ /** Facet options from the build's facts: one per distinct value, with how many rentals carry it. */
2019
+ type FacetOptions = (facts: RentalFactsMap, key: string, options?: FacetOptionsOptions) => FacetOption[];
2020
+ /** BUILD TIME, opt-in: hits per boolean key against a baseline (default "adults=1"), one
2021
+ * rate-limited request per key, sequential; feed the result to `resolveFilters`. A key whose
2022
+ * count the backend does not state is left out (never guessed as 0). */
2023
+ type MeasureFilters = (sdk: WebsiteSdk, keys: readonly string[], options: {
2024
+ readonly locale: Locale;
2025
+ readonly baseline?: string;
2026
+ }) => Promise<FilterMeasurement>;
2027
+ /** See {@link ResolveFilters}. */
2028
+ declare const resolveFilters: ResolveFilters;
2029
+ /** See {@link ReadSelection}. */
2030
+ declare const readSelection: ReadSelection;
2031
+ /** See {@link WriteSelection}. */
2032
+ declare const writeSelection: WriteSelection;
2033
+ /** See {@link MinimumSteps}. */
2034
+ declare const minimumSteps: MinimumSteps;
2035
+ /** See {@link ActiveFilters}. */
2036
+ declare const activeFilters: ActiveFilters;
2037
+ /** See {@link WithoutFilter}. */
2038
+ declare const withoutFilter: WithoutFilter;
2039
+ /** See {@link WithoutFilters}. */
2040
+ declare const withoutFilters: WithoutFilters;
2041
+ /** See {@link QuickFilterDefs}. */
2042
+ declare const quickFilterDefs: QuickFilterDefs;
2043
+ /** See {@link FacetOptions}. */
2044
+ declare const facetOptions: FacetOptions;
2045
+ /** See {@link MeasureFilters}. */
2046
+ declare const measureFilters: MeasureFilters;
2047
+ //#endregion
2048
+ //#region src/search/offers.d.ts
2049
+ /** An offers page's window, as portfolio facts measured by the site (never a lead-time rule). */
2050
+ interface OffersWindowSpec {
2051
+ /** Days from today to the stay the window centres on. Required (portfolio fact). */
2052
+ readonly startOffsetDays: number;
2053
+ /** Stay length in nights. Required (a weekly portfolio answers 3 nights with almost nothing). */
2054
+ readonly nights: number;
2055
+ /** ± days around the stay. Default 14. */
2056
+ readonly flexDays?: number;
2057
+ /** Default 2 (a display start the guest changes in the search bar). */
2058
+ readonly adults?: number;
2059
+ /** Days from today the window may start at earliest ("arrive today" reads as unrealistic).
2060
+ * Default 1. */
2061
+ readonly earliestArrivalOffset?: number;
2062
+ /** An absolute first arrival day (a campaign start found at build): the window opens there and
2063
+ * the stay sits `flexDays` into it, so every stay it can return is a campaign stay. */
2064
+ readonly seasonStart?: IsoDay;
2065
+ }
2066
+ /** A campaign's nearest window found by the build-time sweep (the shortcut links, the chip row). */
2067
+ interface OfferWindow {
2068
+ /** campaignOf(label) — the group the chip names. */
2069
+ readonly campaign: string;
2070
+ /** Exact PMS labels seen in this window for the campaign (verbatim; never an amount). */
2071
+ readonly labels: readonly string[];
2072
+ /** The probed stay: check-in at the sweep offset, check-out `nights` later. */
2073
+ readonly start: IsoDay;
2074
+ /** Check-out, `nights` after `start`. */
2075
+ readonly end: IsoDay;
2076
+ /** The stay length the sweep priced. */
2077
+ readonly nights: number;
2078
+ /** ± days the link widens the stay by. */
2079
+ readonly flexDays: number;
2080
+ /** The party the link asks for. */
2081
+ readonly adults: number;
2082
+ }
2083
+ /** The build-time sweep's grid and cost caps. */
2084
+ interface FindOfferWindowsOptions {
2085
+ /** The language of the labels asked for. */
2086
+ readonly locale: Locale;
2087
+ /** Stay length probed. Required (portfolio fact). */
2088
+ readonly nights: number;
2089
+ /** Days from today each probe window starts at. Default [0, 90, 180, 270] (a coarse scan
2090
+ * grid, not a claim about the customer). */
2091
+ readonly offsets?: readonly number[];
2092
+ /** Window width in days per offset. Default 30. */
2093
+ readonly spanDays?: number;
2094
+ /** Default 2. */
2095
+ readonly adults?: number;
2096
+ /** ± days written into each window's link. Default 14. */
2097
+ readonly flexDays?: number;
2098
+ /** Pages per offset (cost cap; a capped offset is logged, never silently partial). Default 8. */
2099
+ readonly maxPages?: number;
2100
+ /** Default campaignGroup. */
2101
+ readonly campaignOf?: CampaignOf;
2102
+ /** Default today (injected for tests). */
2103
+ readonly today?: IsoDay;
2104
+ }
2105
+ /** The season probe of one campaign. */
2106
+ interface FindCampaignStartOptions {
2107
+ /** The language of the labels asked for. */
2108
+ readonly locale: Locale;
2109
+ /** The campaign group (campaignOf) whose first bookable week is wanted. */
2110
+ readonly campaign: string;
2111
+ /** Exact stays of this length, probed one week apart. Default 7. */
2112
+ readonly nights?: number;
2113
+ /** How far ahead to probe. Default 180 days. */
2114
+ readonly horizonDays?: number;
2115
+ /** Default 2. */
2116
+ readonly adults?: number;
2117
+ /** Default campaignGroup. */
2118
+ readonly campaignOf?: CampaignOf;
2119
+ /** Default today (injected for tests). */
2120
+ readonly today?: IsoDay;
2121
+ }
2122
+ /** One chip of the campaign row. A campaign at home in the current window toggles `?campaign=`
2123
+ * in place; one at home elsewhere is a link carrying its period ("a consequence that rewrites
2124
+ * dates must be visible before the click"); guest-picked dates and the selected campaign always
2125
+ * stay toggles. No counts on chips (the page's count follows the map; a second number would
2126
+ * contradict it). */
2127
+ type CampaignChip = {
2128
+ readonly kind: "toggle";
2129
+ readonly campaign: string;
2130
+ readonly selected: boolean;
2131
+ } | {
2132
+ /** At home in another window: a link that also moves the dates. */
2133
+ readonly kind: "link";
2134
+ /** The campaign group. */
2135
+ readonly campaign: string;
2136
+ /** The campaign's home search (`offerWindowHref`). */
2137
+ readonly href: string;
2138
+ /** The home window as `formatStayRange` prints it (visible before the click). */
2139
+ readonly period: string;
2140
+ };
2141
+ /** What `campaignChips` reads. */
2142
+ interface CampaignChipsInput {
2143
+ /** From the store: the current search, the campaign selected, the offer labels on the results. */
2144
+ readonly state: Pick<SearchState, "search" | "shape" | "campaign" | "offerLabels">;
2145
+ /** findOfferWindows output (build time), may be []. */
2146
+ readonly windows: readonly OfferWindow[];
2147
+ /** The offers page route the links carry. */
2148
+ readonly action: string;
2149
+ /** The site's search contract. */
2150
+ readonly contract: SearchContract;
2151
+ /** The language of the period text. */
2152
+ readonly locale: Locale;
2153
+ /** Default campaignGroup. */
2154
+ readonly campaignOf?: CampaignOf;
2155
+ }
2156
+ /**
2157
+ * Strip a TRAILING percentage only, group on what remains. Joining on the customer's copy is the
2158
+ * feed's weakness (no campaign id on `discount.applied[]`), so the rule is the narrowest that
2159
+ * merges one campaign's discount levels: it fails by NOT grouping (two chips, visible, harmless),
2160
+ * never by merging two campaigns (invisible, untrue). A label that is only a percentage keeps it
2161
+ * (a chip with no name is worse than one with a number).
2162
+ */
2163
+ type CampaignGroup = (label: string) => string;
2164
+ /**
2165
+ * The query an offers page searches today: start/end + minNights = maxNights + flex — the ± shape
2166
+ * both backends execute (v9 maps the pair onto the hub's nights_min/nights_max). Pinning the
2167
+ * length is the point: the same window without the pair is priced as one long stay. The lower
2168
+ * bound clamps to the earliest arrival, the upper never (the picker restores the stay from
2169
+ * end − flex). Throws on a spec no window can come from (a build error, not an empty page).
2170
+ */
2171
+ type OffersWindowQuery = (spec: OffersWindowSpec, today: IsoDay) => string;
2172
+ /**
2173
+ * BUILD TIME, opt-in. One `walkSearchPages` per offset, sequential (cost = offsets × pages
2174
+ * requests per build under the SDK's rate limit; a site without a campaign page does not call
2175
+ * it); one entry per campaign at its NEAREST offset, nearest first — a later offset of the same
2176
+ * campaign is the same campaign. [] when nothing is discounted (a nav entry then hides).
2177
+ */
2178
+ type FindOfferWindows = (sdk: WebsiteSdk, options: FindOfferWindowsOptions) => Promise<OfferWindow[]>;
2179
+ /**
2180
+ * BUILD TIME. The first arrival day (an exact stay of `nights`, probed one week apart) on which
2181
+ * the campaign is offered, or null within the horizon; feeds `OffersWindowSpec.seasonStart`.
2182
+ * Exact stays, not a window: a window answers with each rental's EARLIEST stay, which a
2183
+ * pre-campaign week would win. Cost: one request (the first page) per week until the first hit.
2184
+ * A failed week is logged and skipped, never read as "not running".
2185
+ */
2186
+ type FindCampaignStart = (sdk: WebsiteSdk, options: FindCampaignStartOptions) => Promise<IsoDay | null>;
2187
+ /** The search an offer window links to — its ± window, its party, and `campaign=` — written by
2188
+ * the one URL writer, so the search bar reads it back as the ± stay it is. */
2189
+ type OfferWindowHref = (action: string, window: OfferWindow, contract: SearchContract) => string;
2190
+ /**
2191
+ * The chip row, campaigns in first-seen order: what the results here carry first (toggles), then
2192
+ * the campaigns at home elsewhere (links, nearest first). A campaign is offered once — a toggle or
2193
+ * a link, never both. On a window the sweep found, a campaign whose home is another window links
2194
+ * there even when a result here carries it (every chip then means "show me this campaign where it
2195
+ * runs"); on dates the guest picked, the row only filters what they searched. Pure.
2196
+ */
2197
+ type CampaignChips = (input: CampaignChipsInput) => CampaignChip[];
2198
+ /** See {@link CampaignGroup}. */
2199
+ declare const campaignGroup: CampaignGroup;
2200
+ /** See {@link OffersWindowQuery}. */
2201
+ declare const offersWindowQuery: OffersWindowQuery;
2202
+ /** See {@link FindOfferWindows}. */
2203
+ declare const findOfferWindows: FindOfferWindows;
2204
+ /** See {@link FindCampaignStart}. */
2205
+ declare const findCampaignStart: FindCampaignStart;
2206
+ /** See {@link OfferWindowHref}. */
2207
+ declare const offerWindowHref: OfferWindowHref;
2208
+ /** See {@link CampaignChips}. */
2209
+ declare const campaignChips: CampaignChips;
2210
+ //#endregion
2211
+ //#region src/search/outcome.d.ts
2212
+ /** What the guest set, so an empty state can say what found nothing. */
2213
+ interface SearchedParts {
2214
+ /** Adults + children. */
2215
+ readonly guests: number;
2216
+ /** A period was searched. */
2217
+ readonly dated: boolean;
2218
+ /** A dog was searched. */
2219
+ readonly pets: boolean;
2220
+ /** Applied filters, facets counted per value (dogs are `pets`). */
2221
+ readonly filters: number;
2222
+ }
2223
+ /** One way out of an empty result, as a concrete query guaranteed to differ from the current one. */
2224
+ interface Relaxation {
2225
+ /** Which part of the search it drops. */
2226
+ readonly kind: "without-filters" | "without-pets" | "without-dates" | "fewer-guests";
2227
+ /** The query to navigate to (without "?"). "without-dates" removes EVERY period key. */
2228
+ readonly search: string;
2229
+ /** "fewer-guests": the party it offers. */
2230
+ readonly guests?: number;
2231
+ }
2232
+ /** Why a result is empty, most actionable first (a filter is the narrower, cheaper thing to change). */
2233
+ type EmptyCause = "filters" | "pets" | "dates" | "party";
2234
+ /** The state the results area must explain instead of rendering cards. */
2235
+ type SearchOutcome = {
2236
+ readonly kind: "error";
2237
+ } | {
2238
+ /** Nothing to list. */
2239
+ readonly kind: "empty";
2240
+ /** "offers": nothing reduced in this period (normal much of the year); "ids": the page's
2241
+ * fixed set matched nothing. */
2242
+ readonly scope: "search" | "favorites-none-saved" | "favorites-none-match" | "offers" | "ids";
2243
+ /** What may have found nothing, most actionable first ([] for an empty Merkliste). */
2244
+ readonly causes: readonly EmptyCause[];
2245
+ /** What the guest set. */
2246
+ readonly searched: SearchedParts;
2247
+ /** One way out per cause. */
2248
+ readonly relaxations: readonly Relaxation[];
2249
+ };
2250
+ /** What `describeOutcome` reads. */
2251
+ interface OutcomeInput {
2252
+ /** The walk ended with results, or failed without any. */
2253
+ readonly status: "ready" | "error";
2254
+ /** Results after scopes and filters (not the map viewport: an empty map view is not an empty search). */
2255
+ readonly visibleCount: number;
2256
+ /** The current query. */
2257
+ readonly search: string;
2258
+ /** The page's resolved filters. */
2259
+ readonly filters: ResolvedFilters | null;
2260
+ /** The page's scope (`SearchScope`). */
2261
+ readonly scope: "search" | "favorites" | "offers" | "ids";
2262
+ /** Favourites scope: how many ids are saved. */
2263
+ readonly savedCount?: number;
2264
+ }
2265
+ /** Describe an outcome; null when there are results to show. Pure. An empty Merkliste has no
2266
+ * causes (nothing was searched); every other empty state names its causes in order. */
2267
+ type DescribeOutcome = (input: OutcomeInput) => SearchOutcome | null;
2268
+ /** See {@link DescribeOutcome}. */
2269
+ declare const describeOutcome: DescribeOutcome;
2270
+ //#endregion
2271
+ //#region src/search/sort.d.ts
2272
+ /** recommended: backend order, no sort sent · price-asc/-desc: priced query only, sent AND
2273
+ * re-applied in the browser over the merged list (the SDK orders exact and alternatives
2274
+ * separately, which breaks at the seam) · rating, area-desc, area-asc, guests, bedrooms:
2275
+ * browser only, missing values last · random: browser only, seeded per page view (the v9 hub
2276
+ * ignores `{ by: "random" }`, v10 keeps its order). */
2277
+ type SortKey = "recommended" | "price-asc" | "price-desc" | "rating" | "area-desc" | "area-asc" | "guests" | "bedrooms" | "random";
2278
+ /** Whether an option can be chosen now, and why not. */
2279
+ interface SortOptionState {
2280
+ /** The ordering. */
2281
+ readonly key: SortKey;
2282
+ /** It can be chosen for the current results. */
2283
+ readonly available: boolean;
2284
+ /** "needs-price": the query is not priced. "needs-data": no result carries the value. */
2285
+ readonly reason?: "needs-price" | "needs-data";
2286
+ }
2287
+ /** The platform's short list, the same on every site that does not choose its own. */
2288
+ declare const DEFAULT_SORT_OPTIONS: readonly SortKey[];
2289
+ /** URL values, kept from the fleet's vocabulary so shared links keep working. Any known value
2290
+ * in the URL is honoured even when the site does not offer it in the control. */
2291
+ declare const SORT_URL_VALUE: Readonly<Record<SortKey, string>>;
2292
+ /** The sort in the URL, or "recommended" when absent or unknown (a stale value never errors). */
2293
+ type SortFromSearch = (search: string) => SortKey;
2294
+ /** The SDK ordering for a key on this query shape, or undefined (price needs a priced query: the
2295
+ * SDK rejects it before transport otherwise). */
2296
+ type ToSdkSort = (key: SortKey, shape: QueryShape) => SearchSearchSort | undefined;
2297
+ /** Order results; returns a new array, stable, missing values last; "recommended" keeps the
2298
+ * input order (it IS the backend's ordering). */
2299
+ type SortResults = <T extends SearchResultItem>(items: readonly T[], key: SortKey, seed: number) => T[];
2300
+ /** See {@link SortFromSearch}. */
2301
+ declare const sortFromSearch: SortFromSearch;
2302
+ /** See {@link ToSdkSort}. */
2303
+ declare const toSdkSort: ToSdkSort;
2304
+ /** See {@link SortResults}. */
2305
+ declare const sortResults: SortResults;
2306
+ //#endregion
2307
+ //#region src/search/query.d.ts
2308
+ /** What `sdk.live.search.search` receives for one URL query. */
2309
+ interface SdkSearchQuery {
2310
+ /** Allowlist: period keys this backend executes, occupancy (adults, children, childrenAges,
2311
+ * babies, petsCount), and the resolved filters' sdk keys. Every other key is withheld (it
2312
+ * stays in the URL). Without "?". */
2313
+ readonly query: string;
2314
+ /** Present only when the backend can run the ordering for this query (price on a priced one). */
2315
+ readonly sort?: SearchSearchSort;
2316
+ /** Keys present and deliberately not sent (logs/tests only; nothing renders it). */
2317
+ readonly withheld: readonly string[];
2318
+ }
2319
+ /** The site's search contract plus what the resolved filters decide. */
2320
+ interface SdkQueryOptions extends SearchContract {
2321
+ /** The ordering in the URL (only price reaches the SDK, on a priced query). */
2322
+ readonly sort: SortKey;
2323
+ /** `ResolvedFilters.sdkKeys`. Absent: only period and occupancy reach the SDK. */
2324
+ readonly sdkFilterKeys?: ReadonlySet<string>;
2325
+ /** `FilterSpec.retired`: still executed by the backend, known to match nothing → stripped, so
2326
+ * an old link cannot empty the page invisibly. */
2327
+ readonly retiredKeys?: ReadonlySet<string>;
2328
+ }
2329
+ /** What kind of search a URL is. Read by the store, the card (alternative badge, window line),
2330
+ * the sort control (price gate) and the outcome. Describes what the SDK is asked, so a pair
2331
+ * the contract strips (`nightRangeSearch: false`) does not make a window. */
2332
+ interface QueryShape {
2333
+ /** A fixed start+end stay (not a window, not flexible): only here is a non-exact result an
2334
+ * "alternative" — on a window or flexible search the alternatives ARE the answer. */
2335
+ readonly exactDates: boolean;
2336
+ /** An executable period AND ≥ 1 adult (v9 flexible keys do not count): totals arrive, price
2337
+ * sort possible. */
2338
+ readonly priced: boolean;
2339
+ /** An executable period of any kind is present (priced or not). */
2340
+ readonly dated: boolean;
2341
+ /** Page-level stay length: exact → length; flexible → `nights`; pinned pair (min = max) →
2342
+ * that number; else null (each result states its own — a window's length is not a stay). */
2343
+ readonly nights: number | null;
2344
+ /** The window a night-range or v10 `dates` search spans ("bounds", not a stay), else null. */
2345
+ readonly window: {
2346
+ readonly start: IsoDay;
2347
+ readonly end: IsoDay;
2348
+ } | null;
2349
+ /** v9 ± shape (window + pinned pair + flex): the guest's own stay, asked as a second exact
2350
+ * question (the hub answers a window with its EARLIEST stay); null when not this shape or
2351
+ * check-in is the window's first day (the second request would return the same prices). */
2352
+ readonly ownStay: {
2353
+ readonly start: IsoDay;
2354
+ readonly end: IsoDay;
2355
+ } | null;
2356
+ /** The URL carries a real search (any key but tracking parameters). */
2357
+ readonly hasSearch: boolean;
2358
+ }
2359
+ /** What a result link carries besides the party. */
2360
+ interface ResultHandoverOptions {
2361
+ /** An alternative window, the hub's chosen stay, the own-date answer; else the searched dates
2362
+ * (with the night-range pair when they bound a window, so the quote resolves a real stay). */
2363
+ readonly stay?: {
2364
+ readonly start: IsoDay;
2365
+ readonly end: IsoDay;
2366
+ };
2367
+ /** The result's PMS offer labels (`SearchResultItem.price.offerLabels`): written as repeated
2368
+ * `offer=<label>` (max 4, label text only, never an amount), and only when the link carries a
2369
+ * period — the Quote's `campaignLabels="from-search"` reads them back. */
2370
+ readonly offerLabels?: readonly string[];
2371
+ }
2372
+ /** One field of the current search a partner link can carry. */
2373
+ type ExternalSearchSource = "start" | "end" | "nights" | "minNights" | "maxNights" | "adults" | "children" | "pets";
2374
+ /** A link to ANOTHER search (a partner's portal tile): `base` + one parameter per entry of
2375
+ * `params`, filled from the current search's shape — start/end are the window bounds on a ±
2376
+ * search (exact dates otherwise), nights the stay length; an entry without a value (no dates,
2377
+ * no party) is omitted, never written empty. */
2378
+ interface ExternalSearchLink {
2379
+ /** Absolute https URL; may carry fixed parameters (`?region=…`). */
2380
+ readonly base: string;
2381
+ /** Partner parameter name → the field of the current search it carries. */
2382
+ readonly params: Readonly<Record<string, ExternalSearchSource>>;
2383
+ /** How start/end are spelled. Default "iso". */
2384
+ readonly dateFormat?: "iso" | "d.m.yyyy" | "dd.mm.yyyy";
2385
+ }
2386
+ /** What `sanitizeSearch` returns. */
2387
+ interface SanitizedSearch {
2388
+ /** The cleaned query, without "?" (the input unchanged when nothing was dropped). */
2389
+ readonly search: string;
2390
+ /** The keys removed. */
2391
+ readonly dropped: readonly string[];
2392
+ }
2393
+ /**
2394
+ * URL query → SDK input. Rules: legacy `pets` is never sent (a solo one becomes `petsCount`;
2395
+ * with both, only `petsCount`: the SDK rejects the pair on presence); `weekend=1` → bare
2396
+ * `weekend`; the night-range pair only as a valid pair beside start+end, never beside a flexible
2397
+ * key, and only with `nightRangeSearch`; start/end only as a readable pair; empty values dropped.
2398
+ * Pure: the same input always yields the same query.
2399
+ */
2400
+ type ToSdkQuery = (search: string, options: SdkQueryOptions) => SdkSearchQuery;
2401
+ /** Classify a URL query for this contract. Pure. */
2402
+ type QueryShapeOf = (search: string, contract: SearchContract) => QueryShape;
2403
+ /** The `?…` a result link carries: the period, the party (dogs as `pets`) and the offer labels;
2404
+ * "" when nothing applies. */
2405
+ type HandoverQuery = (search: string, options?: ResultHandoverOptions) => string;
2406
+ /** The partner link for the current search (see `ExternalSearchLink`). Throws on a `base` that is
2407
+ * not an absolute URL (a wrong partner link must fail the build, not ship). */
2408
+ type ExternalSearchHref = (link: ExternalSearchLink, search: string, contract: SearchContract) => string;
2409
+ /** Drop an unsearchable stay (past start, end ≤ start, unparsable, half a pair of dates) with its
2410
+ * night range and `flex`, instead of an error page (the store writes it back with "replace"). */
2411
+ type SanitizeSearch = (search: string, today: IsoDay) => SanitizedSearch;
2412
+ /** Inline `<head>` script source: sets `html[data-results-query]` before first paint when the
2413
+ * URL carries a real search, so a prerendered listing never flashes under a search it does not
2414
+ * answer. Built from the one tracking list (`TRACKING_PARAM_PATTERN`). */
2415
+ type ResultsQueryFlagScript = () => string;
2416
+ /** See {@link ToSdkQuery}. */
2417
+ declare const toSdkQuery: ToSdkQuery;
2418
+ /** See {@link QueryShapeOf}. */
2419
+ declare const queryShape: QueryShapeOf;
2420
+ /** See {@link HandoverQuery}. */
2421
+ declare const handoverQuery: HandoverQuery;
2422
+ /** See {@link ExternalSearchHref}. */
2423
+ declare const externalSearchHref: ExternalSearchHref;
2424
+ /** See {@link SanitizeSearch}. */
2425
+ declare const sanitizeSearch: SanitizeSearch;
2426
+ /** See {@link ResultsQueryFlagScript}. */
2427
+ declare const resultsQueryFlagScript: ResultsQueryFlagScript;
2428
+ //#endregion
2429
+ //#region src/search/store.d.ts
2430
+ /** Narrowing of the one result set. Applied in order: ids → favourites → offers; browser filters;
2431
+ * requireFacts; the URL's `?campaign=`; viewport; sort. */
2432
+ interface SearchScope {
2433
+ /** A fixed set (theme, category, Anlage, landing page), from the build. */
2434
+ readonly ids?: readonly string[];
2435
+ /** The guest's Merkliste. */
2436
+ readonly favorites?: boolean;
2437
+ /** Discounted results only; `labels` narrows to exact PMS labels; `campaign` to a whole campaign
2438
+ * through `campaignOf` (every percentage level of it); `window` makes the page search its offers
2439
+ * window when the URL carries no period (written with "replace"). A `?campaign=` in the URL
2440
+ * narrows further (the chip row) unless `campaign` fixes it. */
2441
+ readonly offers?: {
2442
+ readonly labels?: readonly string[];
2443
+ readonly campaign?: string;
2444
+ readonly window?: OffersWindowSpec;
2445
+ };
2446
+ }
2447
+ /** The store's window on the browser URL, injected so headless stays DOM-free. */
2448
+ interface HistoryPort {
2449
+ /** The current query (with or without "?"). */
2450
+ read(): string;
2451
+ /** "push" for a guest's choice (Back undoes it), "replace" for a sort, a sanitised query, a preset. */
2452
+ write(search: string, mode: "push" | "replace"): void;
2453
+ /** Back/forward and other writers; the store re-reads. */
2454
+ subscribe(onChange: () => void): () => void;
2455
+ }
2456
+ /** The favourites store's read side; headless `Favorites` satisfies it. */
2457
+ interface FavoritesPort {
2458
+ /** The saved rental ids. */
2459
+ ids(): ReadonlySet<string>;
2460
+ /** Called on every change; returns the unsubscribe. */
2461
+ subscribe(onChange: () => void): () => void;
2462
+ }
2463
+ /** Offer label → campaign group (default `campaignGroup`). */
2464
+ type CampaignOf = (label: string) => string;
2465
+ /** Everything the page's search needs. */
2466
+ interface SearchStoreOptions extends ResultLinkOptions, SearchContract {
2467
+ /** The page's URL (the store reads and writes the query only through it). */
2468
+ readonly history: HistoryPort;
2469
+ /** The page's resolved filters (build time). Absent: only period and occupancy reach the SDK. */
2470
+ readonly filters?: ResolvedFilters | null;
2471
+ /** The page's narrowing (fixed set, Merkliste, offers). */
2472
+ readonly scope?: SearchScope;
2473
+ /** The Merkliste (required for `scope.favorites`). */
2474
+ readonly favorites?: FavoritesPort;
2475
+ /** Default campaignGroup. */
2476
+ readonly campaignOf?: CampaignOf;
2477
+ /** Applied (replace) when the URL carries no search of its own (a landing page). */
2478
+ readonly preset?: string;
2479
+ /** Drop rentals the build has no facts for (no detail page was built). Default false. */
2480
+ readonly requireFacts?: boolean;
2481
+ /** "complete": nothing shows until every page is in. "first-page": the first page shows at
2482
+ * once; the browser sort, the outcome and the count wait for the rest. Default "complete". */
2483
+ readonly reveal?: "complete" | "first-page";
2484
+ /** The mode the SDK was created in: the same `SdkMode` value the caller passed to `shareSdk` /
2485
+ * `createSdk` ("all-rentals-at-once": one call, no cursor, no limit). Default "default". */
2486
+ readonly sdkMode?: SdkMode;
2487
+ /** Random-sort seed, drawn once per page view by the caller. */
2488
+ readonly seed: number;
2489
+ /** Favourites removal delay (see `SearchState.leaving`). Default FAVORITE_LEAVE_MS; 0 = immediate
2490
+ * (the component passes 0 under prefers-reduced-motion). */
2491
+ readonly leaveMs?: number;
2492
+ /** The clock, for tests (and `sanitizeSearch`'s today). Default `() => new Date()`. */
2493
+ readonly now?: () => Date;
2494
+ }
2495
+ /** The store's load state: loading until the first answer, ready, or error when nothing loaded. */
2496
+ type SearchStatus = "loading" | "ready" | "error";
2497
+ /** The page's narrowing by name, in the store's precedence (`SearchScope`): the favourites, the
2498
+ * offers, a fixed set, or the plain search. */
2499
+ type SearchScopeKind = "search" | "favorites" | "offers" | "ids";
2500
+ /** The snapshot every consumer reads. */
2501
+ interface SearchState {
2502
+ /** "loading" until the first answer, "error" only when nothing loaded. */
2503
+ readonly status: SearchStatus;
2504
+ /** Every page in; false while a first-page reveal loads the rest, and after a later page failed
2505
+ * (what loaded stays: a partial set beats an error screen). */
2506
+ readonly complete: boolean;
2507
+ /** The current query (sanitised), without "?". */
2508
+ readonly search: string;
2509
+ /** What kind of search `search` is. */
2510
+ readonly shape: QueryShape;
2511
+ /** The page's narrowing (`options.scope`) by name; the count's suffix and the outcome's copy
2512
+ * follow it, so a satellite never restates it. */
2513
+ readonly scope: SearchScopeKind;
2514
+ /** After scopes, filters and the campaign, before the viewport: what the map plots. */
2515
+ readonly matched: readonly SearchResultItem[];
2516
+ /** After the viewport and the sort: what the list renders (leaving favourites included). */
2517
+ readonly items: readonly SearchResultItem[];
2518
+ /** The listed results (leaving favourites excluded); the count follows the map's viewport. */
2519
+ readonly count: number;
2520
+ /** Every result is an alternative on an exact search (the notice); waits for `complete`. */
2521
+ readonly allAlternatives: boolean;
2522
+ /** The ordering in the URL. */
2523
+ readonly sort: SortKey;
2524
+ /** Every `SortKey` with whether it can be chosen now. */
2525
+ readonly sortOptions: readonly SortOptionState[];
2526
+ /** The resolved filters the store was created with. */
2527
+ readonly filters: ResolvedFilters | null;
2528
+ /** The applied filters (chips), hidden ones included, led by the implicit `?property=` and
2529
+ * `?region=` scopes the curation has no facet for (`scope` set): every narrowing the URL applies
2530
+ * is visible and removable. */
2531
+ readonly activeFilters: readonly ActiveFilter[];
2532
+ /** Live counts over the listed set from the facts: a fact flag under its key, a facet value
2533
+ * under `key=value` (each counted without its own filter, so an OR option never reads 0
2534
+ * because a sibling is selected). */
2535
+ readonly filterCounts: Readonly<Record<string, number>>;
2536
+ /** Distinct PMS offer labels of the scoped results, first-seen, before `?campaign=` narrows
2537
+ * (the chips are built from them, so a selection can still switch). */
2538
+ readonly offerLabels: readonly string[];
2539
+ /** The campaign group in scope (the page's, else the URL's), or null. */
2540
+ readonly campaign: string | null;
2541
+ /** Null while results show or the walk has not settled. */
2542
+ readonly outcome: SearchOutcome | null;
2543
+ /** Keys the backend reported as having had no effect. */
2544
+ readonly unusedFilterKeys: readonly string[];
2545
+ /** The card the guest hovers or focuses (the map lights its pin). */
2546
+ readonly highlighted: string | null;
2547
+ /** The map's 220 ms re-settle pulse after a guest move (grid and count show skeletons). */
2548
+ readonly reloading: boolean;
2549
+ /** A viewport filter is active (the map drives the list). */
2550
+ readonly viewportActive: boolean;
2551
+ /** Favourites scope only: ids just un-hearted. They stay in `items` (the card renders
2552
+ * `data-state="leaving"` and animates out) for FAVORITE_LEAVE_MS, are already gone from
2553
+ * `matched` (the pin drops at once) and from `count`; then they leave `items`. Re-hearting
2554
+ * within the window cancels the removal. */
2555
+ readonly leaving: ReadonlySet<string>;
2556
+ }
2557
+ /** The favourites-page removal animation window (the kits' 280 ms). */
2558
+ declare const FAVORITE_LEAVE_MS: 280;
2559
+ /** What every consumer may ask of the store. */
2560
+ interface SearchActions {
2561
+ /** Change the query. A new live search runs only when the SDK-facing part changed (period,
2562
+ * occupancy, sdk filters, an id restriction, a server sort while incomplete); facts filters,
2563
+ * scopes, the campaign and a browser sort re-derive from the loaded set without a request. */
2564
+ setSearch(search: string, mode?: "push" | "replace"): void;
2565
+ /** Order in place ("replace" in the URL). */
2566
+ setSort(key: SortKey): void;
2567
+ /** The filter panel's selection (push). */
2568
+ applyFilters(selection: FilterSelection): void;
2569
+ /** Remove one chip (push). */
2570
+ removeFilter(filter: ActiveFilter): void;
2571
+ /** Remove every filter, hidden ones and the implicit scopes included (push). */
2572
+ clearFilters(): void;
2573
+ /** The ids inside the map bounds, or null for no viewport filter (results without coords
2574
+ * always stay: they cannot leave a view they are not in). */
2575
+ setViewport(ids: ReadonlySet<string> | null): void;
2576
+ /** The map's re-settle pulse (`reloading`). */
2577
+ setReloading(on: boolean): void;
2578
+ /** Drop the ?region= scope (push) so neighbouring results come back (map "release-on-move"). */
2579
+ releaseRegionScope(): void;
2580
+ /** Select a campaign group (replace), or null for every campaign in scope. */
2581
+ setCampaign(group: string | null): void;
2582
+ /** The card the guest hovers or focuses. */
2583
+ setHighlighted(id: string | null): void;
2584
+ /** Re-run the same search after an error. */
2585
+ retry(): void;
2586
+ }
2587
+ /** The store: a snapshot, its subscription and the actions. */
2588
+ interface SearchStore extends Store<SearchState>, SearchActions {
2589
+ /** Stop listening to the URL and the Merkliste, drop a running walk and pending removals. The
2590
+ * SDK stays (the caller owns it). */
2591
+ dispose(): void;
2592
+ }
2593
+ /** The page's one live search. Never creates or disposes the SDK. */
2594
+ type CreateSearchStore = (source: SdkSource, options: SearchStoreOptions) => SearchStore;
2595
+ /** See {@link CreateSearchStore}. */
2596
+ declare const createSearchStore: CreateSearchStore;
2597
+ //#endregion
2598
+ //#region src/map/controller.d.ts
2599
+ /** The search store, structurally (`createSearchStore`'s store satisfies it). */
2600
+ type MapResultsSource = Pick<SearchStore, "getState" | "subscribe" | "setViewport" | "setReloading" | "releaseRegionScope">;
2601
+ /** Cluster settings for the GeoJSON source. */
2602
+ interface ClusterOptions {
2603
+ /** px. Default 54. */
2604
+ readonly radius?: number;
2605
+ /** Clusters exist up to this zoom; points are individual from +1. Default 11. */
2606
+ readonly maxZoom?: number;
2607
+ /** Override for result sets whose pins carry prices (pills are ~70 px, dots 20: priced pills
2608
+ * stacked into one unreadable slab at the dot settings). */
2609
+ readonly whenPriced?: {
2610
+ readonly radius?: number;
2611
+ readonly maxZoom?: number;
2612
+ };
2613
+ }
2614
+ /** The kit's cluster settings (count-only bubbles). */
2615
+ declare const DEFAULT_CLUSTER: {
2616
+ readonly radius: 54;
2617
+ readonly maxZoom: 11;
2618
+ };
2619
+ /** The results map's behaviour switches. */
2620
+ interface ResultsMapOptions {
2621
+ /** The list shows only results in the visible map area once the guest moved the map ("search
2622
+ * as you move"). Default true. */
2623
+ readonly listFollowsMap?: boolean;
2624
+ /** "release-on-move": the first guest move drops a ?region= scope so the neighbouring results
2625
+ * (already loaded) come back. Default "keep". */
2626
+ readonly regionScope?: "keep" | "release-on-move";
2627
+ /** Default "fan". */
2628
+ readonly sameSpot?: SameSpotMode;
2629
+ /** The first frame (padding, maxZoom, outliers). Default DEFAULT_FIT. */
2630
+ readonly fit?: FitOptions;
2631
+ /** The pins' price statement (the card's decision; a site may round pins only). */
2632
+ readonly pins?: PinLabelOptions;
2633
+ }
2634
+ /** What a surface reports after every `moveend` and on every size change. */
2635
+ interface MapViewReport {
2636
+ /** null when the surface has no size (CSS-hidden, a closed layer). */
2637
+ readonly bounds: MapBounds | null;
2638
+ /** The guest moved this surface (pointer, wheel, touch, key, a zoom button) since the previous
2639
+ * report. A programmatic fit is never the guest's. */
2640
+ readonly byGuest: boolean;
2641
+ }
2642
+ /** Which surface reports: the map in the page, or its fullscreen layer. */
2643
+ type ResultsMapSurface = "inline" | "fullscreen";
2644
+ /** The controller's snapshot. */
2645
+ interface ResultsMapState {
2646
+ /** "waiting": search not ready (no tiles are fetched); "empty": nothing to pin; "ready". */
2647
+ readonly status: "waiting" | "empty" | "ready";
2648
+ /** The pins of `matched`, in result order. */
2649
+ readonly points: readonly MapPoint[];
2650
+ /** Some pin carries a price (the cluster's `whenPriced` applies). */
2651
+ readonly priced: boolean;
2652
+ /** Recomputed once the component measured a pin (`setPinWidth`). */
2653
+ readonly layout: SameSpotLayout;
2654
+ /** Where a surface built now (or refitting) frames itself. */
2655
+ readonly frame: MapBounds | null;
2656
+ /** Increments when surfaces must refit to `frame`; never after a guest move. */
2657
+ readonly frameVersion: number;
2658
+ /** The rental whose popup is open. */
2659
+ readonly openId: string | null;
2660
+ /** The "group"-mode spot fanned out on a circle. */
2661
+ readonly expandedGroup: string | null;
2662
+ /** The guest moved a surface: refitting stops, the list may follow the map. */
2663
+ readonly guestMoved: boolean;
2664
+ }
2665
+ /** The controller: its snapshot plus what surfaces and markers report. */
2666
+ interface ResultsMapController extends Store<ResultsMapState> {
2667
+ /** A surface's bounds after a move or a resize (rules 1–4, 6). */
2668
+ report(surface: ResultsMapSurface, view: MapViewReport): void;
2669
+ /** The measured width of the widest pin body (re-lays the same-spot circles). */
2670
+ setPinWidth(px: number): void;
2671
+ /** Open a rental's popup. */
2672
+ openPin(id: string): void;
2673
+ /** Close the popup. */
2674
+ closePopup(): void;
2675
+ /** Toggle one group; null collapses (map click, zoom start). */
2676
+ toggleGroup(key: string | null): void;
2677
+ /** Detach; the list stops following (a map that is gone does not filter). */
2678
+ dispose(): void;
2679
+ }
2680
+ /** The results map's behaviour as a store (rules 1–6 in the file header). */
2681
+ type CreateResultsMapController = (source: MapResultsSource, options?: ResultsMapOptions) => ResultsMapController;
2682
+ /** See {@link CreateResultsMapController}. */
2683
+ declare const createResultsMapController: CreateResultsMapController;
2684
+ //#endregion
2685
+ //#region src/map/places.d.ts
2686
+ /** A fixed place on a map. */
2687
+ interface MapPlace {
2688
+ /** Shown in the popup and the marker label. */
2689
+ readonly name: string;
2690
+ /** Latitude. */
2691
+ readonly lat: number;
2692
+ /** Longitude. */
2693
+ readonly lng: number;
2694
+ }
2695
+ /** One marker on a place map. */
2696
+ interface PlaceMarker {
2697
+ /** The first place's coordinates. */
2698
+ readonly at: LngLat;
2699
+ /** Names of every place on this spot, in input order. */
2700
+ readonly names: readonly string[];
2701
+ }
2702
+ /** Places within SAME_SPOT_METERS of a marker's first place join that marker. */
2703
+ type MergePlaces = (places: readonly MapPlace[]) => PlaceMarker[];
2704
+ /** See {@link MergePlaces}. */
2705
+ declare const mergePlaces: MergePlaces;
2706
+ //#endregion
2707
+ //#region src/map/address.d.ts
2708
+ /** "street": street, postcode and town. "town": the town only. It decides the printed line only;
2709
+ * the marker stays on the coordinates the PMS publishes (a coarsened marker would protect
2710
+ * nothing while the results map and the page data carry the same coordinates). */
2711
+ type RentalAddressMode = "street" | "town";
2712
+ /** The address line, or null when the feed states nothing. "street" without street data falls
2713
+ * back to the town (less than was decided, never more). Throws `RentalAddressModeError` for any
2714
+ * other mode value (an undecided page fails its build). */
2715
+ type RentalAddressLine = (rental: Pick<RentalDetail, "address" | "town">, mode: RentalAddressMode) => string | null;
2716
+ /** The address decision is missing or not one of the two modes (named, so a build log says
2717
+ * which decision is open). */
2718
+ declare class RentalAddressModeError extends Error {
2719
+ constructor(mode: unknown);
2720
+ }
2721
+ /** See {@link RentalAddressLine}. */
2722
+ declare const rentalAddressLine: RentalAddressLine;
2723
+ //#endregion
2724
+ export { describeOutcome as $, RentalFlag as $i, fetchRentalFlags as $n, CardSource as $r, GuestBound as $t, QueryShape as A, SdkMode as Ai, LngLat as An, SdkConfigForInput as Ar, quickFilterDefs as At, toSdkQuery as B, RentalDistance as Bi, resultCardSource as Bn, RouteParams as Br, SEARCH_PARAMS as Bt, SearchState as C, isIsoDay as Ci, SameSpotGroup as Cn, EnvRecord as Cr, ResolvedFilters as Ct, createSearchStore as D, Locale as Di, circleOffsets as Dn, HUB_ENDPOINTS as Dr, facetOptions as Dt, SearchStoreOptions as E, Backend as Ei, SameSpotOptions as En, sdkConfigFromEnv as Er, activeFilters as Et, externalSearchHref as F, WebsiteSdk as Fi, mapPoints as Fn, CardSourceExtras as Fr, writeSelection as Ft, sortFromSearch as G, parseCounts as Gi, PreviewStrategy as Gn, CardBadge as Gr, SearchQuery as Gt, SORT_URL_VALUE as H, distanceFact as Hi, shortReviewerName as Hn, routeHref as Hr, SearchParamAliases as Ht, handoverQuery as I, resolveSdk as Ii, ResultLinkOptions as In, cardSource as Ir, partyPairs as It, EmptyCause as J, BedRun as Ji, DEFAULT_FLAG_FILTERS as Jn, CardImage as Jr, searchHref as Jt, sortResults as K, parseDistances as Ki, descriptionPreview as Kn, CardContext as Kr, WriteSearchOptions as Kt, queryShape as L, ADJACENT_BELOW_M as Li, SearchResultItem as Ln, loadRentalFacts as Lr, readParty as Lt, SanitizedSearch as M, SdkSetup as Mi, MapPoint as Mn, VofficeEndpointTable as Mr, resolveFilters as Mt, SdkQueryOptions as N, SdkSource as Ni, PinLabelOptions as Nn, VofficeEndpoints as Nr, withoutFilter as Nt, ExternalSearchLink as O, STAGES as Oi, distanceMeters as On, HubEndpointTable as Or, measureFilters as Ot, SdkSearchQuery as P, Stage as Pi, hasMapPoints as Pn, sdkConfigFor as Pr, withoutFilters as Pt, SearchedParts as Q, RentalDetail as Qi, assignSlugs as Qn, CardRating as Qr, DEFAULT_GUEST_LIMITS as Qt, resultsQueryFlagScript as R, DistanceKind as Ri, catalogueResultItem as Rn, rentalFactsMap as Rr, FlexibleStay as Rt, SearchScopeKind as S, formatIsoDate as Si, PixelOffset as Sn, createSdk as Sr, ResolveFiltersOptions as St, SearchStore as T, todayIso as Ti, SameSpotMode as Tn, SdkEnvError as Tr, WhenEmpty as Tt, SortKey as U, isAdjacent as Ui, splitReviewAuthor as Un, AlternativePeriod as Ur, SearchParamKeys as Ut, DEFAULT_SORT_OPTIONS as V, WATER_KINDS as Vi, newestReviewsFirst as Vn, RouteTemplate as Vr, SearchContract as Vt, SortOptionState as W, isSpecEcho as Wi, DescriptionPreview as Wn, BadgeRule as Wr, SearchPeriod as Wt, Relaxation as X, DescriptionContent as Xi, LoadRentalCatalogueOptions as Xn, CardModelOptions as Xr, ChildAgeRange as Xt, OutcomeInput as Y, CatalogueRental as Yi, FetchRentalFlagsOptions as Yn, CardLiveField as Yr, writeSearch as Yt, SearchOutcome as Z, RentalAddress as Zi, RentalFlagsResult as Zn, CardPill as Zr, DEFAULT_CHILD_AGE as Zt, FAVORITE_LEAVE_MS as _, EnhancePart as _a, Glyph as _i, DEFAULT_FIT as _n, fetchSearchPage as _r, FilterGroupSpec as _t, PlaceMarker as a, ReviewCriterion as aa, RentalFactsMap as ai, Party as an, AmenityPreviewItem as ar, OffersWindowSpec as at, SearchActions as b, GateMarkers as ba, IsoMonth as bi, frameFor as bn, SharedSdk as br, FilterSpec as bt, DEFAULT_CLUSTER as c, PlainTextOptions as ca, toCardModel as ci, clampParty as cn, AdaptEvent as cr, findCampaignStart as ct, ResultsMapController as d, StructuredDocumentOptions as da, Range as di, stayKeyOf as dn, adaptRental as dr, offersWindowQuery as dt, RentalImage as ea, CardStatus as ei, GuestBounds as en, loadRentalCatalogue as er, CampaignChip as et, ResultsMapOptions as f, TextMarkup as fa, matches as fi, Listener as fn, RawSearchPage as fr, ActiveFilter as ft, CampaignOf as g, ENHANCE_PARTS as ga, GLYPHS as gi, createStore as gn, WalkSummary as gr, FilterEntry as gt, createResultsMapController as h, plainText as ha, parsePriceLabel as hi, WritableStore as hn, WalkOptions as hr, FilterDef as ht, MapPlace as i, RentalRoom as ia, RentalFacts as ii, GuestOptions as in, ALWAYS_EXCLUDED_CATEGORIES as ir, OfferWindow as it, ResultHandoverOptions as j, SdkOptions as ji, MapBounds as jn, VOFFICE_ENDPOINTS as jr, readSelection as jt, ExternalSearchSource as k, SdkConfig as ki, layoutSameSpot as kn, HubEndpoints as kr, minimumSteps as kt, MapResultsSource as l, RichBlock as la, CardMatch as li, guestBounds as ln, AdaptRentalOptions as lr, findOfferWindows as lt, ResultsMapSurface as m, parseTextBlocks as ma, formatPriceLabel as mi, Unsubscribe as mn, SearchPageRequest as mr, FacetOptionsOptions as mt, RentalAddressModeError as n, RentalReview as na, PillOptions as ni, GuestKind as nn, slugify as nr, FindCampaignStartOptions as nt, mergePlaces as o, ReviewSummary as oa, StayPrice as oi, QuoteOccupancy as on, AmenityPreviewOptions as or, campaignChips as ot, ResultsMapState as p, normalizeStructuredDocument as pa, PriceCents as pi, Store as pn, SEARCH_PAGE_SIZE as pr, FacetOption as pt, toSdkSort as q, AmenityGroup as qi, descriptionSummary as qn, CardFact as qr, readSearch as qt, rentalAddressLine as r, RentalReviews as ra, RentalCardModel as ri, GuestLimits as rn, HIDDEN_ROOM_ATTRIBUTES as rr, FindOfferWindowsOptions as rt, ClusterOptions as s, RoomKind as sa, StayWindow as si, agesComplete as sn, amenityPreview as sr, campaignGroup as st, RentalAddressMode as t, RentalPropertyInfo as ta, FactKind as ti, GuestCaps as tn, rankHighlights as tr, CampaignChipsInput as tt, MapViewReport as u, RichInline as ua, CardSignal as ui, occupancyOf as un, RentalContentError as ur, offerWindowHref as ut, FavoritesPort as v, EnhanceParts as va, GlyphList as vi, DefaultFit as vn, walkSearchPages as vr, FilterMeasurement as vt, SearchStatus as w, nightsBetween as wi, SameSpotLayout as wn, SDK_ENV as wr, SdkFilterDefinition as wt, SearchScope as x, addDays as xi, idsInView as xn, shareSdk as xr, ImplicitScope as xt, HistoryPort as y, GATE_MARKERS as ya, IsoDay as yi, FitOptions as yn, loadSharedSdk as yr, FilterSelection as yt, sanitizeSearch as z, RentalCounts as zi, compareUndatedSearchOrder as zn, withRentalFacts as zr, QueryPair as zt };