export function extractZillowStructuredPhotoCandidatesFromNextDataScript(scriptText) { if (typeof scriptText !== "string" || !scriptText.trim()) { return []; } let nextData; try { nextData = JSON.parse(scriptText); } catch { return []; } const cacheText = nextData?.props?.pageProps?.componentProps?.gdpClientCache; if (typeof cacheText !== "string" || !cacheText.trim()) { return []; } let cache; try { cache = JSON.parse(cacheText); } catch { return []; } const out = []; for (const entry of Object.values(cache)) { const photos = entry?.property?.responsivePhotos; if (!Array.isArray(photos)) continue; for (const photo of photos) { if (typeof photo?.url === "string" && photo.url) { out.push({ url: photo.url }); continue; } const mixedSources = photo?.mixedSources; if (!mixedSources || typeof mixedSources !== "object") continue; let best = null; for (const variants of Object.values(mixedSources)) { if (!Array.isArray(variants)) continue; for (const variant of variants) { if (typeof variant?.url !== "string" || !variant.url) continue; const width = Number(variant.width || 0); if (!best || width > best.width) { best = { url: variant.url, width }; } } } if (best) { out.push(best); } } } return out; } function collapseIdentifier(value) { return String(value || "").replace(/\s+/g, " ").trim(); } function isLikelyIdentifier(value) { return /^[A-Z0-9-]{4,40}$/i.test(collapseIdentifier(value)); } function visitForIdentifierHints(node, hints) { if (!node || typeof node !== "object") return; if (Array.isArray(node)) { for (const item of node) { visitForIdentifierHints(item, hints); } return; } for (const [key, value] of Object.entries(node)) { const normalizedKey = key.toLowerCase(); if ((normalizedKey === "parcelid" || normalizedKey === "parcelnumber") && hints.parcelId == null) { if (typeof value === "string" || typeof value === "number") { const candidate = collapseIdentifier(value); if (isLikelyIdentifier(candidate)) { hints.parcelId = candidate; } } } if ((normalizedKey === "apn" || normalizedKey === "apnnumber" || normalizedKey === "taxparcelid" || normalizedKey === "taxid") && hints.apn == null) { if (typeof value === "string" || typeof value === "number") { const candidate = collapseIdentifier(value); if (isLikelyIdentifier(candidate)) { hints.apn = candidate; } } } if (value && typeof value === "object") { visitForIdentifierHints(value, hints); } } } export function extractZillowIdentifierHintsFromNextDataScript(scriptText) { if (typeof scriptText !== "string" || !scriptText.trim()) { return {}; } let nextData; try { nextData = JSON.parse(scriptText); } catch { return {}; } const hints = {}; visitForIdentifierHints(nextData, hints); const cacheText = nextData?.props?.pageProps?.componentProps?.gdpClientCache; if (typeof cacheText === "string" && cacheText.trim()) { try { visitForIdentifierHints(JSON.parse(cacheText), hints); } catch { // Ignore cache parse failures; base next-data parse already succeeded. } } return hints; } export function extractZillowIdentifierHintsFromText(text) { const source = typeof text === "string" ? text : ""; const hints = {}; const parcelMatch = source.match(/\b(?:parcel|parcel number|parcel #|tax parcel)(?:\s*(?:number|#|no\.?))?\s*[:#]?\s*([A-Z0-9-]{4,40})\b/i); if (parcelMatch) { hints.parcelId = collapseIdentifier(parcelMatch[1]); } const apnMatch = source.match(/\b(?:apn|apn #|apn no\.?|tax id)(?:\s*(?:number|#|no\.?))?\s*[:#]?\s*([A-Z0-9-]{4,40})\b/i); if (apnMatch) { hints.apn = collapseIdentifier(apnMatch[1]); } return hints; } const DEFAULT_MINIMUM_TRUSTED_STRUCTURED_PHOTO_COUNT = 12; export function shouldUseStructuredZillowPhotos(candidates, options = {}) { const count = Array.isArray(candidates) ? candidates.length : 0; const normalizedOptions = typeof options === "number" ? { expectedPhotoCount: options } : options && typeof options === "object" ? options : {}; const expectedPhotoCount = Number(normalizedOptions.expectedPhotoCount || 0); const fallbackPhotoCount = Number(normalizedOptions.fallbackPhotoCount || 0); const minimumTrustCount = Number( normalizedOptions.minimumTrustCount || DEFAULT_MINIMUM_TRUSTED_STRUCTURED_PHOTO_COUNT ); if (Number.isFinite(expectedPhotoCount) && expectedPhotoCount > 0) { return count >= expectedPhotoCount; } if (Number.isFinite(fallbackPhotoCount) && fallbackPhotoCount > 0) { return count >= fallbackPhotoCount; } return count >= minimumTrustCount; }