webcrawl 1.2.3 → 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dist/cli.js +3 -2
  2. package/dist/core/engine/cache.d.ts +70 -0
  3. package/dist/core/engine/cache.js +297 -0
  4. package/dist/core/engine/crawl-policy.d.ts +36 -0
  5. package/dist/core/engine/crawl-policy.js +134 -0
  6. package/dist/core/engine/crawler.d.ts +151 -0
  7. package/dist/core/engine/crawler.js +885 -0
  8. package/dist/core/engine/fetcher.d.ts +66 -0
  9. package/dist/core/engine/fetcher.js +185 -0
  10. package/dist/core/engine/index.d.ts +4 -0
  11. package/dist/core/engine/index.js +4 -0
  12. package/dist/core/engine/markdown.d.ts +33 -0
  13. package/dist/core/engine/markdown.js +334 -0
  14. package/dist/core/engine/path-scope.d.ts +21 -0
  15. package/dist/core/engine/path-scope.js +88 -0
  16. package/dist/core/engine/safe-url.d.ts +23 -0
  17. package/dist/core/engine/safe-url.js +92 -0
  18. package/dist/core/engine/stealth.d.ts +79 -0
  19. package/dist/core/engine/stealth.js +155 -0
  20. package/dist/core/engine.d.ts +1 -0
  21. package/dist/core/engine.js +1 -0
  22. package/dist/core/extract/business.d.ts +28 -0
  23. package/dist/core/extract/business.js +344 -0
  24. package/dist/core/extract/currency.d.ts +15 -0
  25. package/dist/core/extract/currency.js +259 -0
  26. package/dist/core/extract/cve-cache.d.ts +62 -0
  27. package/dist/core/extract/cve-cache.js +277 -0
  28. package/dist/core/extract/cve.d.ts +57 -0
  29. package/dist/core/extract/cve.js +397 -0
  30. package/dist/core/extract/dedupe-key.d.ts +12 -0
  31. package/dist/core/extract/dedupe-key.js +36 -0
  32. package/dist/core/extract/flights.d.ts +37 -0
  33. package/dist/core/extract/flights.js +373 -0
  34. package/dist/core/extract/hotels.d.ts +35 -0
  35. package/dist/core/extract/hotels.js +473 -0
  36. package/dist/core/extract/index.d.ts +8 -0
  37. package/dist/core/extract/index.js +8 -0
  38. package/dist/core/extract/real-estate.d.ts +53 -0
  39. package/dist/core/extract/real-estate.js +835 -0
  40. package/dist/core/extract/scaledown.d.ts +14 -0
  41. package/dist/core/extract/scaledown.js +484 -0
  42. package/dist/core/extract/shopping.d.ts +44 -0
  43. package/dist/core/extract/shopping.js +393 -0
  44. package/dist/core/image-url.d.ts +5 -0
  45. package/dist/core/image-url.js +13 -0
  46. package/dist/core/interact/driver.d.ts +49 -0
  47. package/dist/core/interact/driver.js +264 -0
  48. package/dist/core/interact/remote.d.ts +41 -0
  49. package/dist/core/interact/remote.js +133 -0
  50. package/dist/core/interact/spans.d.ts +46 -0
  51. package/dist/core/interact/spans.js +32 -0
  52. package/dist/core/interact/validate.d.ts +34 -0
  53. package/dist/core/interact/validate.js +135 -0
  54. package/dist/core/interact/vm.d.ts +42 -0
  55. package/dist/core/interact/vm.js +296 -0
  56. package/dist/core/local-engine.d.ts +35 -0
  57. package/dist/core/local-engine.js +367 -0
  58. package/dist/core/parse/detect.d.ts +29 -0
  59. package/dist/core/parse/detect.js +152 -0
  60. package/dist/core/parse/fetch.d.ts +20 -0
  61. package/dist/core/parse/fetch.js +39 -0
  62. package/dist/core/parse/image.d.ts +13 -0
  63. package/dist/core/parse/image.js +100 -0
  64. package/dist/core/parse/index.d.ts +53 -0
  65. package/dist/core/parse/index.js +153 -0
  66. package/dist/core/parse/limits.d.ts +12 -0
  67. package/dist/core/parse/limits.js +29 -0
  68. package/dist/core/parse/media.d.ts +42 -0
  69. package/dist/core/parse/media.js +238 -0
  70. package/dist/core/parse/merge.d.ts +19 -0
  71. package/dist/core/parse/merge.js +48 -0
  72. package/dist/core/parse/ocr.d.ts +25 -0
  73. package/dist/core/parse/ocr.js +121 -0
  74. package/dist/core/parse/office.d.ts +28 -0
  75. package/dist/core/parse/office.js +83 -0
  76. package/dist/core/parse/pdf.d.ts +27 -0
  77. package/dist/core/parse/pdf.js +156 -0
  78. package/dist/core/parse/types.d.ts +38 -0
  79. package/dist/core/parse/types.js +1 -0
  80. package/dist/core/parse/youtube.d.ts +30 -0
  81. package/dist/core/parse/youtube.js +264 -0
  82. package/dist/core/ranker/decision.d.ts +28 -0
  83. package/dist/core/ranker/decision.js +151 -0
  84. package/dist/core/ranker/index.d.ts +5 -0
  85. package/dist/core/ranker/index.js +361 -0
  86. package/dist/core/ranker/lexical.d.ts +82 -0
  87. package/dist/core/ranker/lexical.js +642 -0
  88. package/dist/core/ranker/types.d.ts +90 -0
  89. package/dist/core/ranker/types.js +1 -0
  90. package/dist/core/ranker.d.ts +1 -0
  91. package/dist/core/ranker.js +1 -0
  92. package/dist/core/sandbox.d.ts +29 -0
  93. package/dist/core/sandbox.js +182 -0
  94. package/dist/core/search/catalog.d.ts +174 -0
  95. package/dist/core/search/catalog.js +488 -0
  96. package/dist/core/search/index.d.ts +3 -0
  97. package/dist/core/search/index.js +3 -0
  98. package/dist/core/search/pipeline.d.ts +7 -0
  99. package/dist/core/search/pipeline.js +704 -0
  100. package/dist/core/search/providers.d.ts +62 -0
  101. package/dist/core/search/providers.js +1175 -0
  102. package/dist/core/search.d.ts +1 -0
  103. package/dist/core/search.js +1 -0
  104. package/dist/core/targets/developer.d.ts +44 -0
  105. package/dist/core/targets/developer.js +338 -0
  106. package/dist/core/targets/index.d.ts +3 -0
  107. package/dist/core/targets/index.js +3 -0
  108. package/dist/core/targets/registry.d.ts +32 -0
  109. package/dist/core/targets/registry.js +411 -0
  110. package/dist/core/targets/types.d.ts +72 -0
  111. package/dist/core/targets/types.js +8 -0
  112. package/dist/core/targets.d.ts +1 -0
  113. package/dist/core/targets.js +1 -0
  114. package/package.json +82 -4
@@ -0,0 +1,373 @@
1
+ import { extractDomainHost, parsePriceString, resolveCurrencyCode, } from "./currency.js";
2
+ import { canonicalResultUrl } from "./dedupe-key.js";
3
+ import { resolveAbsoluteUrl } from "./real-estate.js";
4
+ const KNOWN_AIRLINES = [
5
+ "Qantas",
6
+ "Virgin Australia",
7
+ "Jetstar",
8
+ "Rex",
9
+ "Air New Zealand",
10
+ "United Airlines",
11
+ "United",
12
+ "Delta Air Lines",
13
+ "Delta",
14
+ "American Airlines",
15
+ "Southwest Airlines",
16
+ "Southwest",
17
+ "Alaska Airlines",
18
+ "JetBlue",
19
+ "Spirit Airlines",
20
+ "Frontier Airlines",
21
+ "British Airways",
22
+ "EasyJet",
23
+ "Ryanair",
24
+ "Lufthansa",
25
+ "Air France",
26
+ "KLM",
27
+ "Emirates",
28
+ "Qatar Airways",
29
+ "Singapore Airlines",
30
+ "Cathay Pacific",
31
+ "ANA",
32
+ "Japan Airlines",
33
+ "Air Canada",
34
+ "Turkish Airlines",
35
+ "Etihad Airways",
36
+ "Swiss",
37
+ "Iberia",
38
+ "Finnair",
39
+ "Scoot",
40
+ "Fiji Airways",
41
+ "Hawaiian Airlines",
42
+ "Malaysia Airlines",
43
+ "Thai Airways",
44
+ "Korean Air",
45
+ ];
46
+ const AIRLINE_REGEX = new RegExp(`\\b(${KNOWN_AIRLINES.map((a) => a.replace(/\s+/g, "\\s+")).join("|")})\\b`, "i");
47
+ export function inferRouteFromText(text) {
48
+ // Check parenthesized IATA codes like "Sydney (SYD) to Los Angeles (LAX)"
49
+ const parenIata = text.match(/\(([A-Z]{3})\)[^()]{1,40}?\b(?:to|->|→|–|-|\u2014)\b[^()]{1,40}?\(([A-Z]{3})\)/i);
50
+ if (parenIata && parenIata[1].toUpperCase() !== parenIata[2].toUpperCase()) {
51
+ return {
52
+ origin: parenIata[1].toUpperCase(),
53
+ destination: parenIata[2].toUpperCase(),
54
+ };
55
+ }
56
+ // Check IATA pair like "SYD to LAX", "SYD - MEL", "SYD → LHR", "(JFK) to (SFO)"
57
+ const iataMatch = text.match(/\(?([A-Z]{3})\)?\s*(?:to|->|→|–|-|\u2014)\s*\(?([A-Z]{3})\)?/);
58
+ if (iataMatch &&
59
+ iataMatch[1] !== iataMatch[2] &&
60
+ !/^(USD|AUD|GBP|EUR|CAD|NZD|SGD|JPY|INR|THE|AND|FOR|NON|ALL|TOP|LOW|FLY)$/.test(iataMatch[1]) &&
61
+ !/^(USD|AUD|GBP|EUR|CAD|NZD|SGD|JPY|INR|THE|AND|FOR|NON|ALL|TOP|LOW|FLY)$/.test(iataMatch[2])) {
62
+ return { origin: iataMatch[1], destination: iataMatch[2] };
63
+ }
64
+ // Check "from <Origin> to <Destination>"
65
+ const fromToMatch = text.match(/\b(?:flights?\s+from|from)\s+([A-Z][a-zA-Z\s]{2,22}?)\s+to\s+([A-Z][a-zA-Z\s]{2,22}?)(?=\s+(?:from|under|for|starting|on|in|with|\$|£|€|A\$|at|\||-|$))/i);
66
+ if (fromToMatch) {
67
+ return {
68
+ origin: fromToMatch[1].trim(),
69
+ destination: fromToMatch[2].trim(),
70
+ };
71
+ }
72
+ // Check "<Origin> to <Destination> Flights"
73
+ const cityToCity = text.match(/\b([A-Z][a-zA-Z]+(?:\s+[A-Z][a-zA-Z]+)?)\s+to\s+([A-Z][a-zA-Z]+(?:\s+[A-Z][a-zA-Z]+)?)\s+flights?\b/i);
74
+ if (cityToCity) {
75
+ const o = cityToCity[1].trim();
76
+ const d = cityToCity[2].trim();
77
+ if (!/^(cheap|direct|nonstop|best|book|compare|find)$/i.test(o)) {
78
+ return { origin: o, destination: d };
79
+ }
80
+ }
81
+ return {};
82
+ }
83
+ export function inferCabinClass(text) {
84
+ const lower = text.toLowerCase();
85
+ if (/\bfirst\s+class\b/.test(lower)) {
86
+ return "first";
87
+ }
88
+ if (/\bbusiness\s+class\b/.test(lower)) {
89
+ return "business";
90
+ }
91
+ if (/\bpremium\s+economy\b/.test(lower)) {
92
+ return "premium_economy";
93
+ }
94
+ if (/\beconomy\b/.test(lower)) {
95
+ return "economy";
96
+ }
97
+ return undefined;
98
+ }
99
+ export function inferTripType(text) {
100
+ const lower = text.toLowerCase();
101
+ if (/\b(one[- ]way)\b/.test(lower)) {
102
+ return "one_way";
103
+ }
104
+ if (/\b(round[- ]trip|return)\b/.test(lower)) {
105
+ return "round_trip";
106
+ }
107
+ return undefined;
108
+ }
109
+ export function inferDurationAndStops(text) {
110
+ const durMatch = text.match(/\b(\d{1,2}\s*h(?:rs?|ours?)?(?:\s*\d{1,2}\s*m(?:ins?)?)?)\b/i);
111
+ let stops;
112
+ if (/\b(non[- ]?stop|direct\s+flights?)\b/i.test(text)) {
113
+ stops = 0;
114
+ }
115
+ else {
116
+ const stopsMatch = text.match(/\b(\d)\s+stops?\b/i);
117
+ if (stopsMatch) {
118
+ stops = Number.parseInt(stopsMatch[1], 10);
119
+ }
120
+ }
121
+ return {
122
+ duration: durMatch ? durMatch[1].replace(/\s+/g, " ").trim() : undefined,
123
+ stops,
124
+ };
125
+ }
126
+ export function parseFlightPrice(rawPrice, countryCode, explicitCurrency) {
127
+ const defaultCurrency = resolveCurrencyCode(explicitCurrency, countryCode);
128
+ if (typeof rawPrice === "number" &&
129
+ Number.isFinite(rawPrice) &&
130
+ rawPrice > 10 &&
131
+ rawPrice < 50_000) {
132
+ const rounded = Math.round(rawPrice);
133
+ return {
134
+ price: rounded,
135
+ priceText: `${defaultCurrency} $${rounded.toLocaleString("en-US")}`,
136
+ currency: defaultCurrency,
137
+ };
138
+ }
139
+ const text = String(rawPrice || "").trim();
140
+ if (!text) {
141
+ return { priceText: "Check Fare", currency: defaultCurrency };
142
+ }
143
+ const parsed = parsePriceString(text, countryCode, explicitCurrency, 50_000);
144
+ if (parsed && parsed.price >= 15) {
145
+ return {
146
+ price: Math.round(parsed.price),
147
+ priceText: text,
148
+ currency: parsed.currency,
149
+ };
150
+ }
151
+ return { priceText: text, currency: defaultCurrency };
152
+ }
153
+ function walkFlightJsonNode(node, pageUrl, portal, countryCode, queryRoute, extractedVia, out, depth = 0) {
154
+ if (depth > 12 || out.length >= 40 || !node || typeof node !== "object") {
155
+ return;
156
+ }
157
+ if (Array.isArray(node)) {
158
+ for (const item of node) {
159
+ walkFlightJsonNode(item, pageUrl, portal, countryCode, queryRoute, extractedVia, out, depth + 1);
160
+ }
161
+ return;
162
+ }
163
+ const obj = node;
164
+ const rawType = String(obj["@type"] || obj.type || "").toLowerCase();
165
+ // Check Schema.org Flight / Offer / Product or state flight object
166
+ const airlineObj = obj.airline ||
167
+ obj.carrier ||
168
+ obj.provider;
169
+ const airlineName = typeof obj.airline === "string"
170
+ ? obj.airline
171
+ : typeof obj.carrier === "string"
172
+ ? obj.carrier
173
+ : typeof obj.airlineName === "string"
174
+ ? obj.airlineName
175
+ : typeof airlineObj?.name === "string"
176
+ ? airlineObj.name
177
+ : undefined;
178
+ const depAirport = obj.departureAirport;
179
+ const arrAirport = obj.arrivalAirport;
180
+ const origin = typeof obj.origin === "string"
181
+ ? obj.origin
182
+ : typeof obj.departureAirport === "string"
183
+ ? obj.departureAirport
184
+ : typeof depAirport?.iataCode === "string"
185
+ ? depAirport.iataCode
186
+ : typeof depAirport?.name === "string"
187
+ ? depAirport.name
188
+ : queryRoute.origin;
189
+ const destination = typeof obj.destination === "string"
190
+ ? obj.destination
191
+ : typeof obj.arrivalAirport === "string"
192
+ ? obj.arrivalAirport
193
+ : typeof arrAirport?.iataCode === "string"
194
+ ? arrAirport.iataCode
195
+ : typeof arrAirport?.name === "string"
196
+ ? arrAirport.name
197
+ : queryRoute.destination;
198
+ const offersObj = obj.offers;
199
+ const rawPriceVal = obj.price ??
200
+ obj.minPrice ??
201
+ obj.lowPrice ??
202
+ obj.totalPrice ??
203
+ offersObj?.price ??
204
+ offersObj?.lowPrice;
205
+ const rawCurrency = (typeof obj.priceCurrency === "string" ? obj.priceCurrency : undefined) ||
206
+ (typeof obj.currency === "string" ? obj.currency : undefined) ||
207
+ (typeof offersObj?.priceCurrency === "string"
208
+ ? offersObj.priceCurrency
209
+ : undefined);
210
+ const isFlightNode = rawType.includes("flight") ||
211
+ Boolean(airlineName && (origin || destination || rawPriceVal !== undefined)) ||
212
+ Boolean(origin && destination && rawPriceVal !== undefined);
213
+ if (isFlightNode) {
214
+ const parsedPrice = parseFlightPrice(rawPriceVal, countryCode, rawCurrency);
215
+ const flightNumber = typeof obj.flightNumber === "string" ||
216
+ typeof obj.flightNumber === "number"
217
+ ? String(obj.flightNumber)
218
+ : undefined;
219
+ const departureTime = typeof obj.departureTime === "string" ? obj.departureTime : undefined;
220
+ const arrivalTime = typeof obj.arrivalTime === "string" ? obj.arrivalTime : undefined;
221
+ const duration = typeof obj.duration === "string"
222
+ ? obj.duration
223
+ : typeof obj.flightDuration === "string"
224
+ ? obj.flightDuration
225
+ : undefined;
226
+ const stops = typeof obj.stops === "number" || typeof obj.stops === "string"
227
+ ? obj.stops
228
+ : typeof obj.numberOfStops === "number"
229
+ ? obj.numberOfStops
230
+ : undefined;
231
+ const itemUrl = typeof obj.url === "string"
232
+ ? resolveAbsoluteUrl(obj.url, pageUrl)
233
+ : pageUrl;
234
+ const routeLabel = origin && destination
235
+ ? `${origin} → ${destination}`
236
+ : origin || destination || "Flight";
237
+ const title = typeof obj.name === "string" && obj.name.trim().length > 4
238
+ ? obj.name.trim()
239
+ : `${airlineName ? `${airlineName} · ` : ""}${routeLabel}`;
240
+ if (parsedPrice.price || airlineName || (origin && destination)) {
241
+ out.push({
242
+ title,
243
+ airline: airlineName,
244
+ flightNumber,
245
+ origin,
246
+ destination,
247
+ departureTime,
248
+ arrivalTime,
249
+ duration,
250
+ stops,
251
+ price: parsedPrice.price,
252
+ priceText: parsedPrice.priceText,
253
+ currency: parsedPrice.currency,
254
+ url: itemUrl,
255
+ provider: extractDomainHost(itemUrl) || portal,
256
+ extractedVia,
257
+ });
258
+ }
259
+ }
260
+ for (const val of Object.values(obj)) {
261
+ if (val && typeof val === "object") {
262
+ walkFlightJsonNode(val, pageUrl, portal, countryCode, queryRoute, extractedVia, out, depth + 1);
263
+ }
264
+ }
265
+ }
266
+ export function extractFlightOffersFromHtml(html, pageUrl, countryCode, queryHint) {
267
+ const out = [];
268
+ const portal = extractDomainHost(pageUrl) || "unknown";
269
+ const queryRoute = inferRouteFromText(`${queryHint || ""} ${pageUrl}`);
270
+ // Tier 1: Schema.org JSON-LD blocks
271
+ const jsonLdRegex = /<script[^>]+type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
272
+ let match;
273
+ while ((match = jsonLdRegex.exec(html)) !== null) {
274
+ try {
275
+ const parsed = JSON.parse(match[1].trim());
276
+ walkFlightJsonNode(parsed, pageUrl, portal, countryCode, queryRoute, "json-ld", out);
277
+ }
278
+ catch {
279
+ // Ignore malformed JSON-LD
280
+ }
281
+ }
282
+ // Tier 2: Embedded JSON state (__NEXT_DATA__, application/json)
283
+ if (out.length < 3) {
284
+ const stateRegex = /<script[^>]+(?:id=["']__NEXT_DATA__["']|type=["']application\/json["'])[^>]*>([\s\S]*?)<\/script>/gi;
285
+ while ((match = stateRegex.exec(html)) !== null) {
286
+ try {
287
+ const raw = match[1].trim();
288
+ if (raw.length > 20 && raw.length < 2_000_000) {
289
+ const parsed = JSON.parse(raw);
290
+ walkFlightJsonNode(parsed, pageUrl, portal, countryCode, queryRoute, "json-state", out);
291
+ }
292
+ }
293
+ catch {
294
+ // Ignore
295
+ }
296
+ }
297
+ }
298
+ return dedupeFlightOffers(out);
299
+ }
300
+ export function extractFlightOfferFromSerpResult(title, description, url, countryCode, queryHint) {
301
+ const combined = `${title} ${description} ${queryHint || ""}`;
302
+ const route = inferRouteFromText(combined);
303
+ const airlineMatch = `${title} ${description}`.match(AIRLINE_REGEX);
304
+ const explicitPriceMatch = `${title} ${description}`.match(/(?:A\$|AU\$|US\$|CA\$|NZ\$|S\$|SG\$|HK\$|Rs\.?|\$|£|€|¥|₹|₫|฿|₱|₩|\b(?:USD|AUD|GBP|EUR|CAD|NZD|SGD|INR|VND|THB|MYR|IDR|CHF|AED)\b)\s*(\d[\d.,]*)|(\d[\d.,]*)\s*(?:€|£|₫|₹|\b(?:EUR|GBP|INR|SGD|VND|CHF)\b)/i);
305
+ const priceParsed = explicitPriceMatch
306
+ ? parsePriceString(explicitPriceMatch[0], countryCode, undefined, 25_000)
307
+ : null;
308
+ const { duration, stops } = inferDurationAndStops(`${title} ${description}`);
309
+ const cabinClass = inferCabinClass(combined);
310
+ const tripType = inferTripType(combined);
311
+ // Require an actual fare price or specific airline in the snippet
312
+ if ((!priceParsed || priceParsed.price < 25) && !airlineMatch) {
313
+ return null;
314
+ }
315
+ const defaultCurrency = resolveCurrencyCode(undefined, countryCode);
316
+ const price = priceParsed && priceParsed.price >= 25
317
+ ? Math.round(priceParsed.price)
318
+ : undefined;
319
+ const currency = priceParsed?.currency || defaultCurrency;
320
+ const priceText = price
321
+ ? `${currency} $${price.toLocaleString("en-US")}`
322
+ : "Check Live Fare";
323
+ const flightNoMatch = `${title} ${description}`.match(/\b([A-Z]{2}\s?\d{2,4})\b/);
324
+ const provider = extractDomainHost(url) || "unknown";
325
+ return {
326
+ title: title.replace(/\s*[|\-–\u2014•]\s*.*$/, "").trim() ||
327
+ `${route.origin || "Flight"} → ${route.destination || ""}`.trim(),
328
+ airline: airlineMatch ? airlineMatch[1] : undefined,
329
+ flightNumber: flightNoMatch &&
330
+ !/^(US|AU|UK|EU|IN|CA|up|to|in|on)\s?\d+$/i.test(flightNoMatch[1])
331
+ ? flightNoMatch[1].replace(/\s+/g, "")
332
+ : undefined,
333
+ origin: route.origin,
334
+ destination: route.destination,
335
+ duration,
336
+ stops,
337
+ cabinClass,
338
+ tripType,
339
+ price,
340
+ priceText,
341
+ currency,
342
+ url,
343
+ provider,
344
+ extractedVia: "serp-snippet",
345
+ };
346
+ }
347
+ export function dedupeFlightOffers(offers, maxItems = 30) {
348
+ const seen = new Set();
349
+ const out = [];
350
+ for (const offer of offers) {
351
+ const key = `${(offer.airline || "").toLowerCase()}|${(offer.origin || "").toLowerCase()}|${(offer.destination || "").toLowerCase()}|${offer.price || offer.priceText}|${offer.provider}|${canonicalResultUrl(offer.url)}`;
352
+ if (seen.has(key)) {
353
+ continue;
354
+ }
355
+ seen.add(key);
356
+ out.push(offer);
357
+ if (out.length >= maxItems) {
358
+ break;
359
+ }
360
+ }
361
+ return out.sort((a, b) => {
362
+ if (a.price !== undefined && b.price !== undefined) {
363
+ return a.price - b.price;
364
+ }
365
+ if (a.price !== undefined) {
366
+ return -1;
367
+ }
368
+ if (b.price !== undefined) {
369
+ return 1;
370
+ }
371
+ return 0;
372
+ });
373
+ }
@@ -0,0 +1,35 @@
1
+ export interface HotelListing {
2
+ name: string;
3
+ address?: string;
4
+ city?: string;
5
+ country?: string;
6
+ pricePerNight?: number;
7
+ priceText: string;
8
+ currency?: string;
9
+ starRating?: number;
10
+ guestRating?: number;
11
+ reviewCount?: number;
12
+ amenities?: string[];
13
+ roomType?: string;
14
+ freeCancellation?: boolean;
15
+ image?: string;
16
+ latitude?: number;
17
+ longitude?: number;
18
+ url: string;
19
+ provider: string;
20
+ extractedVia: "json-state" | "rsc-payload" | "json-ld" | "html-cards" | "scaledown-extract-1" | "serp-snippet";
21
+ }
22
+ export declare function parseHotelPrice(rawPrice: string | number | undefined | null, countryCode?: string, explicitCurrency?: string): {
23
+ pricePerNight?: number;
24
+ priceText: string;
25
+ currency: string;
26
+ };
27
+ export declare function extractHotelListingsFromHtml(html: string, pageUrl: string, countryCode?: string): HotelListing[];
28
+ /**
29
+ * Booking.com property cards. District pages use `data-testid="card"`, search
30
+ * results use `data-testid="property-card"`; both carry `data-testid="title"`,
31
+ * a `titleLink`, an `image`, a star aria-label and a "guest rating" line.
32
+ */
33
+ export declare function extractBookingCards(html: string, pageUrl: string, portal: string, countryCode?: string): HotelListing[];
34
+ export declare function extractHotelListingFromSerpResult(title: string, description: string, url: string, countryCode?: string): HotelListing | null;
35
+ export declare function dedupeHotelListings(hotels: HotelListing[], maxItems?: number): HotelListing[];