Use Zillow parcel hints for CAD lookup

This commit is contained in:
2026-03-28 03:55:56 -05:00
parent ece8fc548f
commit b77134ced5
11 changed files with 438 additions and 18 deletions

View File

@@ -1,6 +1,7 @@
import os from "node:os";
import path from "node:path";
import { extractZillowIdentifierHints } from "../../web-automation/scripts/zillow-identifiers.js";
import { discoverListingSources, type ListingDiscoveryResult } from "./listing-discovery.js";
import { extractPhotoData, type PhotoExtractionResult, type PhotoSource } from "./photo-review.js";
import { resolvePublicRecords, type PublicRecordsResolution } from "./public-records.js";
@@ -36,6 +37,7 @@ interface AssessPropertyDeps {
renderReportPdfFn?: typeof renderReportPdf;
discoverListingSourcesFn?: typeof discoverListingSources;
extractPhotoDataFn?: typeof extractPhotoData;
extractZillowIdentifierHintsFn?: typeof extractZillowIdentifierHints;
}
interface PurposeGuidance {
@@ -195,14 +197,17 @@ function inferSourceFromUrl(rawUrl: string): PhotoSource | null {
}
}
async function resolvePhotoReview(
options: AssessPropertyOptions,
discoverListingSourcesFn: typeof discoverListingSources,
extractPhotoDataFn: typeof extractPhotoData
): Promise<{
interface ResolvedListingCandidates {
attempts: string[];
listingUrls: Array<{ label: string; url: string }>;
photoReview: Record<string, unknown>;
}> {
zillowUrl: string | null;
harUrl: string | null;
}
async function resolveListingCandidates(
options: AssessPropertyOptions,
discoverListingSourcesFn: typeof discoverListingSources
): Promise<ResolvedListingCandidates> {
const attempts: string[] = [];
const listingUrls: Array<{ label: string; url: string }> = [];
@@ -241,9 +246,27 @@ async function resolvePhotoReview(
addListingUrl("Discovered HAR Listing", harUrl);
}
return {
attempts,
listingUrls,
zillowUrl,
harUrl,
};
}
async function resolvePhotoReview(
listingCandidates: ResolvedListingCandidates,
extractPhotoDataFn: typeof extractPhotoData,
additionalAttempts: string[] = []
): Promise<{
listingUrls: Array<{ label: string; url: string }>;
photoReview: Record<string, unknown>;
}> {
const attempts: string[] = [...listingCandidates.attempts, ...additionalAttempts];
const listingUrls = [...listingCandidates.listingUrls];
const candidates: Array<{ source: PhotoSource; url: string }> = [];
if (zillowUrl) candidates.push({ source: "zillow", url: zillowUrl });
if (harUrl) candidates.push({ source: "har", url: harUrl });
if (listingCandidates.zillowUrl) candidates.push({ source: "zillow", url: listingCandidates.zillowUrl });
if (listingCandidates.harUrl) candidates.push({ source: "har", url: listingCandidates.harUrl });
let extracted: PhotoExtractionResult | null = null;
for (const candidate of candidates) {
@@ -411,21 +434,52 @@ export async function assessProperty(
const renderReportPdfFn = deps.renderReportPdfFn || renderReportPdf;
const discoverListingSourcesFn = deps.discoverListingSourcesFn || discoverListingSources;
const extractPhotoDataFn = deps.extractPhotoDataFn || extractPhotoData;
const extractZillowIdentifierHintsFn =
deps.extractZillowIdentifierHintsFn || extractZillowIdentifierHints;
const listingCandidates = await resolveListingCandidates(
{ ...options, assessmentPurpose: purpose },
discoverListingSourcesFn
);
const identifierAttempts: string[] = [];
let effectiveParcelId = options.parcelId;
if (!effectiveParcelId && listingCandidates.zillowUrl) {
try {
const hints = await extractZillowIdentifierHintsFn(listingCandidates.zillowUrl);
effectiveParcelId = hints.parcelId || hints.apn || effectiveParcelId;
if (Array.isArray(hints.notes) && hints.notes.length) {
identifierAttempts.push(...hints.notes);
}
} catch (error) {
identifierAttempts.push(
`Zillow parcel/APN extraction failed: ${error instanceof Error ? error.message : String(error)}`
);
}
}
const effectiveListingSourceUrl =
options.listingSourceUrl || listingCandidates.zillowUrl || listingCandidates.harUrl || undefined;
const publicRecords = await resolvePublicRecordsFn(options.address, {
parcelId: options.parcelId,
parcelId: effectiveParcelId,
listingGeoId: options.listingGeoId,
listingSourceUrl: options.listingSourceUrl
listingSourceUrl: effectiveListingSourceUrl
});
const photoResolution = await resolvePhotoReview(
{ ...options, assessmentPurpose: purpose },
discoverListingSourcesFn,
extractPhotoDataFn
listingCandidates,
extractPhotoDataFn,
identifierAttempts
);
const reportPayload = buildAssessmentReportPayload(
{ ...options, assessmentPurpose: purpose },
{
...options,
assessmentPurpose: purpose,
parcelId: effectiveParcelId,
listingSourceUrl: effectiveListingSourceUrl
},
publicRecords,
photoResolution.listingUrls,
photoResolution.photoReview

View File

@@ -175,6 +175,16 @@ function buildCadSearchKeywords(address: string, year: number): string {
return `${collapseWhitespace(address)} Year:${year}`.trim();
}
function formatNuecesGeographicId(parcelId: string | null | undefined): string | null {
const normalized = collapseWhitespace(parcelId).replace(/[^0-9]/g, "");
if (!normalized) return null;
if (normalized.length <= 4) return normalized;
if (normalized.length <= 8) {
return `${normalized.slice(0, 4)}-${normalized.slice(4)}`;
}
return `${normalized.slice(0, 4)}-${normalized.slice(4, 8)}-${normalized.slice(8)}`;
}
function parseAddressForCadSearch(address: string): {
rawAddress: string;
streetNumber: string | null;
@@ -327,6 +337,7 @@ function pickBestCadResult(
async function enrichNuecesCadPropertyDetails(
address: string,
appraisalDistrictWebsite: string,
parcelId: string | null | undefined,
fetchText: FetchLike
): Promise<PropertyDetailsResolution | null> {
const parsedAddress = parseAddressForCadSearch(address);
@@ -341,10 +352,12 @@ async function enrichNuecesCadPropertyDetails(
if (!searchToken) return null;
const searchYear = parseCurrentYearFromSearchHome(searchHomeHtml);
const formattedGeographicId = formatNuecesGeographicId(parcelId);
const searchKeywords =
parsedAddress.streetNumber && parsedAddress.streetName
formattedGeographicId ||
(parsedAddress.streetNumber && parsedAddress.streetName
? `StreetNumber:${parsedAddress.streetNumber} StreetName:"${parsedAddress.streetName}"`
: buildCadSearchKeywords(address, searchYear);
: buildCadSearchKeywords(address, searchYear));
const fetchSearchPage = async (page: number): Promise<any> => {
const searchResultsUrl = `${normalizedPropertySearchUrl}/search/SearchResults?keywords=${encodeURIComponent(searchKeywords)}`;
@@ -478,6 +491,7 @@ async function enrichNuecesCadPropertyDetails(
async function tryEnrichPropertyDetails(
address: string,
parcelId: string | null | undefined,
appraisalDistrictWebsite: string | null,
fetchText: FetchLike
): Promise<PropertyDetailsResolution | null> {
@@ -487,7 +501,7 @@ async function tryEnrichPropertyDetails(
const normalizedWebsite = normalizeUrl(website).toLowerCase();
try {
if (normalizedWebsite.includes("nuecescad.net") || normalizedWebsite.includes("ncadistrict.com")) {
return await enrichNuecesCadPropertyDetails(address, website, fetchText);
return await enrichNuecesCadPropertyDetails(address, website, parcelId, fetchText);
}
} catch {
return null;
@@ -729,6 +743,7 @@ export async function resolvePublicRecords(
taxAssessorCollector = offices.taxAssessorCollector;
propertyDetails = await tryEnrichPropertyDetails(
address,
options.parcelId,
typeof offices.appraisalDistrict?.Website === "string"
? offices.appraisalDistrict.Website
: null,