Use Zillow parcel hints for CAD lookup
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user