Fix property assessor geocode fallback
This commit is contained in:
@@ -119,6 +119,8 @@ scripts/property-assessor locate-public-records --address "<street-address>"
|
|||||||
Current behavior:
|
Current behavior:
|
||||||
|
|
||||||
- uses the official Census geocoder
|
- uses the official Census geocoder
|
||||||
|
- when Census address matching misses, falls back to an external address geocoder and then resolves official Census geographies from coordinates
|
||||||
|
- retries fallback geocoding without the unit suffix when a condo/unit-qualified address is too specific for the fallback provider
|
||||||
- returns matched address, county/state/FIPS, and block GEOID context
|
- returns matched address, county/state/FIPS, and block GEOID context
|
||||||
- for Texas, returns:
|
- for Texas, returns:
|
||||||
- Texas Comptroller county directory page
|
- Texas Comptroller county directory page
|
||||||
|
|||||||
@@ -140,6 +140,8 @@ When `--assessment-purpose` is present, it should also:
|
|||||||
|
|
||||||
This command currently:
|
This command currently:
|
||||||
- resolves the address through the official Census geocoder
|
- resolves the address through the official Census geocoder
|
||||||
|
- when Census address matching misses, falls back to an external address geocoder and then resolves official Census geographies from coordinates
|
||||||
|
- retries the fallback geocoder without the unit suffix when a condo/unit-qualified address is too specific for the fallback provider
|
||||||
- returns county/state/FIPS/GEOID context
|
- returns county/state/FIPS/GEOID context
|
||||||
- for Texas, resolves the official Texas Comptroller county directory page
|
- for Texas, resolves the official Texas Comptroller county directory page
|
||||||
- returns the county appraisal district and tax assessor/collector links when available
|
- returns the county appraisal district and tax assessor/collector links when available
|
||||||
|
|||||||
@@ -127,16 +127,16 @@ function normalizePurpose(value: string): string {
|
|||||||
function getPurposeGuidance(purpose: string): PurposeGuidance {
|
function getPurposeGuidance(purpose: string): PurposeGuidance {
|
||||||
const normalized = purpose.toLowerCase();
|
const normalized = purpose.toLowerCase();
|
||||||
|
|
||||||
if (/(invest|rental|cash[\s-]?flow|income|flip|str|airbnb)/i.test(normalized)) {
|
if (/(daughter|son|college|student|school|campus)/i.test(normalized)) {
|
||||||
return {
|
return {
|
||||||
label: purpose,
|
label: purpose,
|
||||||
snapshot: `Purpose fit: evaluate this as an income property first, not a generic owner-occupant home.`,
|
snapshot: "Purpose fit: evaluate this as practical housing for a student, with safety, commute, durability, and oversight burden in mind.",
|
||||||
like: "Purpose-aligned upside will depend on durable rent support, manageable make-ready, and exit liquidity.",
|
like: "This purpose is best served by simple logistics, durable finishes, and a layout that is easy to live in and maintain.",
|
||||||
caution: "An investment property is not attractive just because the address clears basic public-record checks; rent support and margin still decide the deal.",
|
caution: "Student-use housing can become a bad fit when commute friction, fragile finishes, or management burden are underestimated.",
|
||||||
comp: "Comp work should focus on resale liquidity and true rent support for an income property, not only nearby asking prices.",
|
comp: "Comp work should emphasize campus proximity, day-to-day practicality, and whether comparable student-friendly options exist at lower all-in cost.",
|
||||||
carry: "Carry view should underwrite this as an income property with taxes, insurance, HOA, maintenance, vacancy, and management friction.",
|
carry: "Carry view should account for parent-risk factors, furnishing/setup costs, upkeep burden, and whether renting may still be the lower-risk option.",
|
||||||
diligence: "Confirm realistic rent, reserve for turns and repairs, and whether the building/submarket has enough liquidity for an investment property exit.",
|
diligence: "Confirm commute, safety, parking, roommate practicality if relevant, and whether the property is easy to maintain from a distance.",
|
||||||
verdict: `Assessment purpose: ${purpose}. Do not issue a buy/pass conclusion until the property clears rent support, carry, and make-ready standards for an investment property.`
|
verdict: `Assessment purpose: ${purpose}. The final recommendation should prioritize practicality, safety, and management burden over generic resale talking points.`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,16 +153,16 @@ function getPurposeGuidance(purpose: string): PurposeGuidance {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/(daughter|son|college|student|school|campus)/i.test(normalized)) {
|
if (/(invest|rental|cash[\s-]?flow|income|flip|str|airbnb)/i.test(normalized)) {
|
||||||
return {
|
return {
|
||||||
label: purpose,
|
label: purpose,
|
||||||
snapshot: "Purpose fit: evaluate this as practical housing for a student, with safety, commute, durability, and oversight burden in mind.",
|
snapshot: `Purpose fit: evaluate this as an income property first, not a generic owner-occupant home.`,
|
||||||
like: "This purpose is best served by simple logistics, durable finishes, and a layout that is easy to live in and maintain.",
|
like: "Purpose-aligned upside will depend on durable rent support, manageable make-ready, and exit liquidity.",
|
||||||
caution: "Student-use housing can become a bad fit when commute friction, fragile finishes, or management burden are underestimated.",
|
caution: "An investment property is not attractive just because the address clears basic public-record checks; rent support and margin still decide the deal.",
|
||||||
comp: "Comp work should emphasize campus proximity, day-to-day practicality, and whether comparable student-friendly options exist at lower all-in cost.",
|
comp: "Comp work should focus on resale liquidity and true rent support for an income property, not only nearby asking prices.",
|
||||||
carry: "Carry view should account for parent-risk factors, furnishing/setup costs, upkeep burden, and whether renting may still be the lower-risk option.",
|
carry: "Carry view should underwrite this as an income property with taxes, insurance, HOA, maintenance, vacancy, and management friction.",
|
||||||
diligence: "Confirm commute, safety, parking, roommate practicality if relevant, and whether the property is easy to maintain from a distance.",
|
diligence: "Confirm realistic rent, reserve for turns and repairs, and whether the building/submarket has enough liquidity for an investment property exit.",
|
||||||
verdict: `Assessment purpose: ${purpose}. The final recommendation should prioritize practicality, safety, and management burden over generic resale talking points.`
|
verdict: `Assessment purpose: ${purpose}. Do not issue a buy/pass conclusion until the property clears rent support, carry, and make-ready standards for an investment property.`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
export const CENSUS_GEOCODER_URL =
|
export const CENSUS_GEOCODER_URL =
|
||||||
"https://geocoding.geo.census.gov/geocoder/geographies/onelineaddress";
|
"https://geocoding.geo.census.gov/geocoder/geographies/onelineaddress";
|
||||||
|
export const CENSUS_COORDINATES_URL =
|
||||||
|
"https://geocoding.geo.census.gov/geocoder/geographies/coordinates";
|
||||||
|
export const NOMINATIM_SEARCH_URL = "https://nominatim.openstreetmap.org/search";
|
||||||
export const TEXAS_COUNTY_DIRECTORY_URL =
|
export const TEXAS_COUNTY_DIRECTORY_URL =
|
||||||
"https://comptroller.texas.gov/taxes/property-tax/county-directory/";
|
"https://comptroller.texas.gov/taxes/property-tax/county-directory/";
|
||||||
export const TEXAS_PROPERTY_TAX_PORTAL = "https://texas.gov/PropertyTaxes";
|
export const TEXAS_PROPERTY_TAX_PORTAL = "https://texas.gov/PropertyTaxes";
|
||||||
@@ -76,6 +79,28 @@ function stripHtml(value: string): string {
|
|||||||
return output.replace(/^,\s*|\s*,\s*$/g, "");
|
return output.replace(/^,\s*|\s*,\s*$/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildFallbackAddressCandidates(address: string): string[] {
|
||||||
|
const normalized = collapseWhitespace(address);
|
||||||
|
if (!normalized) return [];
|
||||||
|
|
||||||
|
const candidates = [normalized];
|
||||||
|
const [streetPartRaw, ...restParts] = normalized.split(",");
|
||||||
|
const streetPart = collapseWhitespace(streetPartRaw);
|
||||||
|
const locality = restParts.map((part) => collapseWhitespace(part)).filter(Boolean).join(", ");
|
||||||
|
const strippedStreet = collapseWhitespace(
|
||||||
|
streetPart.replace(
|
||||||
|
/\s+(?:apt|apartment|unit|suite|ste)\s*[a-z0-9-]+$/i,
|
||||||
|
""
|
||||||
|
).replace(/\s+#\s*[a-z0-9-]+$/i, "")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (strippedStreet && strippedStreet !== streetPart) {
|
||||||
|
candidates.push(locality ? `${strippedStreet}, ${locality}` : strippedStreet);
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
function extractAnchorHref(fragment: string): string | null {
|
function extractAnchorHref(fragment: string): string | null {
|
||||||
const match = fragment.match(/<a[^>]+href="([^"]+)"/i);
|
const match = fragment.match(/<a[^>]+href="([^"]+)"/i);
|
||||||
if (!match) return null;
|
if (!match) return null;
|
||||||
@@ -87,6 +112,7 @@ function extractAnchorHref(fragment: string): string | null {
|
|||||||
async function geocodeAddress(address: string, fetchText: FetchLike): Promise<{
|
async function geocodeAddress(address: string, fetchText: FetchLike): Promise<{
|
||||||
match: any;
|
match: any;
|
||||||
censusGeocoderUrl: string;
|
censusGeocoderUrl: string;
|
||||||
|
usedFallbackGeocoder: boolean;
|
||||||
}> {
|
}> {
|
||||||
const query = new URLSearchParams({
|
const query = new URLSearchParams({
|
||||||
address,
|
address,
|
||||||
@@ -97,10 +123,70 @@ async function geocodeAddress(address: string, fetchText: FetchLike): Promise<{
|
|||||||
const url = `${CENSUS_GEOCODER_URL}?${query.toString()}`;
|
const url = `${CENSUS_GEOCODER_URL}?${query.toString()}`;
|
||||||
const payload = JSON.parse(await fetchText(url));
|
const payload = JSON.parse(await fetchText(url));
|
||||||
const matches = payload?.result?.addressMatches || [];
|
const matches = payload?.result?.addressMatches || [];
|
||||||
if (!matches.length) {
|
if (matches.length) {
|
||||||
|
return {
|
||||||
|
match: matches[0],
|
||||||
|
censusGeocoderUrl: url,
|
||||||
|
usedFallbackGeocoder: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let fallbackMatch: any = null;
|
||||||
|
for (const candidateAddress of buildFallbackAddressCandidates(address)) {
|
||||||
|
const fallbackQuery = new URLSearchParams({
|
||||||
|
q: candidateAddress,
|
||||||
|
format: "jsonv2",
|
||||||
|
limit: "1",
|
||||||
|
countrycodes: "us",
|
||||||
|
addressdetails: "1"
|
||||||
|
});
|
||||||
|
const fallbackUrl = `${NOMINATIM_SEARCH_URL}?${fallbackQuery.toString()}`;
|
||||||
|
const fallbackPayload = JSON.parse(await fetchText(fallbackUrl));
|
||||||
|
fallbackMatch = Array.isArray(fallbackPayload) ? fallbackPayload[0] : null;
|
||||||
|
if (fallbackMatch) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!fallbackMatch) {
|
||||||
throw new PublicRecordsLookupError(`No Census geocoder match found for address: ${address}`);
|
throw new PublicRecordsLookupError(`No Census geocoder match found for address: ${address}`);
|
||||||
}
|
}
|
||||||
return { match: matches[0], censusGeocoderUrl: url };
|
|
||||||
|
const latitude = Number(fallbackMatch.lat);
|
||||||
|
const longitude = Number(fallbackMatch.lon);
|
||||||
|
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
|
||||||
|
throw new PublicRecordsLookupError(
|
||||||
|
`Fallback geocoder returned invalid coordinates for address: ${address}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const coordinateQuery = new URLSearchParams({
|
||||||
|
x: String(longitude),
|
||||||
|
y: String(latitude),
|
||||||
|
benchmark: "Public_AR_Current",
|
||||||
|
vintage: "Current_Current",
|
||||||
|
format: "json"
|
||||||
|
});
|
||||||
|
const coordinateUrl = `${CENSUS_COORDINATES_URL}?${coordinateQuery.toString()}`;
|
||||||
|
const coordinatePayload = JSON.parse(await fetchText(coordinateUrl));
|
||||||
|
const geographies = coordinatePayload?.result?.geographies;
|
||||||
|
if (!geographies) {
|
||||||
|
throw new PublicRecordsLookupError(
|
||||||
|
`Census coordinate geographies lookup failed for address: ${address}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
match: {
|
||||||
|
matchedAddress: collapseWhitespace(fallbackMatch.display_name || address),
|
||||||
|
coordinates: {
|
||||||
|
x: longitude,
|
||||||
|
y: latitude
|
||||||
|
},
|
||||||
|
geographies
|
||||||
|
},
|
||||||
|
censusGeocoderUrl: coordinateUrl,
|
||||||
|
usedFallbackGeocoder: true
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findTexasCountyHref(countyName: string, fetchText: FetchLike): Promise<string> {
|
async function findTexasCountyHref(countyName: string, fetchText: FetchLike): Promise<string> {
|
||||||
@@ -221,7 +307,10 @@ export async function resolvePublicRecords(
|
|||||||
} = {}
|
} = {}
|
||||||
): Promise<PublicRecordsResolution> {
|
): Promise<PublicRecordsResolution> {
|
||||||
const fetchText = options.fetchText || defaultFetchText;
|
const fetchText = options.fetchText || defaultFetchText;
|
||||||
const { match, censusGeocoderUrl } = await geocodeAddress(address, fetchText);
|
const { match, censusGeocoderUrl, usedFallbackGeocoder } = await geocodeAddress(
|
||||||
|
address,
|
||||||
|
fetchText
|
||||||
|
);
|
||||||
const geographies = match.geographies || {};
|
const geographies = match.geographies || {};
|
||||||
const state = (geographies.States || [{}])[0];
|
const state = (geographies.States || [{}])[0];
|
||||||
const county = (geographies.Counties || [{}])[0];
|
const county = (geographies.Counties || [{}])[0];
|
||||||
@@ -238,6 +327,11 @@ export async function resolvePublicRecords(
|
|||||||
"Try official address search first on the appraisal district site.",
|
"Try official address search first on the appraisal district site.",
|
||||||
"If the listing exposes parcel/APN/account identifiers, use them as stronger search keys than ZPID or listing geo IDs."
|
"If the listing exposes parcel/APN/account identifiers, use them as stronger search keys than ZPID or listing geo IDs."
|
||||||
];
|
];
|
||||||
|
if (usedFallbackGeocoder) {
|
||||||
|
lookupRecommendations.push(
|
||||||
|
"The Census address lookup missed this address, so a fallback geocoder was used to obtain coordinates before resolving official Census geographies."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (state.STUSAB === "TX" && county.NAME) {
|
if (state.STUSAB === "TX" && county.NAME) {
|
||||||
const offices = await fetchTexasCountyOffices(county.NAME, fetchText);
|
const offices = await fetchTexasCountyOffices(county.NAME, fetchText);
|
||||||
|
|||||||
@@ -224,3 +224,45 @@ test("assessProperty renders a PDF when recipient email is present", async () =>
|
|||||||
const stat = await fs.promises.stat(outputPath);
|
const stat = await fs.promises.stat(outputPath);
|
||||||
assert.ok(stat.size > 1000);
|
assert.ok(stat.size > 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("assessProperty prioritizes student housing guidance over investment fallback keywords", async () => {
|
||||||
|
const result = await assessProperty(
|
||||||
|
{
|
||||||
|
address: "1011 Ennis Joslin Rd APT 235, Corpus Christi, TX 78412",
|
||||||
|
assessmentPurpose:
|
||||||
|
"college housing for daughter attending TAMU-CC; prioritize proximity, safety/livability, and resale/rental fallback"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resolvePublicRecordsFn: async () => samplePublicRecords,
|
||||||
|
discoverListingSourcesFn: async () => ({
|
||||||
|
attempts: ["Zillow discovery located a property page from the address."],
|
||||||
|
zillowUrl:
|
||||||
|
"https://www.zillow.com/homedetails/1011-Ennis-Joslin-Rd-APT-235-Corpus-Christi-TX-78412/28848927_zpid/",
|
||||||
|
harUrl: null
|
||||||
|
}),
|
||||||
|
extractPhotoDataFn: async (source, url) => ({
|
||||||
|
source,
|
||||||
|
requestedUrl: url,
|
||||||
|
finalUrl: url,
|
||||||
|
expectedPhotoCount: 20,
|
||||||
|
complete: true,
|
||||||
|
photoCount: 20,
|
||||||
|
imageUrls: ["https://photos.example/1.jpg"],
|
||||||
|
notes: [`${source} extractor succeeded.`]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.match(
|
||||||
|
String(result.reportPayload?.verdict?.offerGuidance),
|
||||||
|
/daughter|student|practicality|safety/i
|
||||||
|
);
|
||||||
|
assert.doesNotMatch(
|
||||||
|
String(result.reportPayload?.verdict?.offerGuidance),
|
||||||
|
/income property|investment property/i
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
String(result.reportPayload?.carryView?.[0]),
|
||||||
|
/parent-risk|upkeep burden|renting/i
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -80,3 +80,119 @@ test("resolvePublicRecords uses Census and Texas county directory", async () =>
|
|||||||
assert.equal(payload.sourceIdentifierHints.parcelId, "14069438");
|
assert.equal(payload.sourceIdentifierHints.parcelId, "14069438");
|
||||||
assert.match(payload.lookupRecommendations.join(" "), /listing geo IDs as regional hints only/i);
|
assert.match(payload.lookupRecommendations.join(" "), /listing geo IDs as regional hints only/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("resolvePublicRecords falls back to coordinate geocoding when Census address lookup misses", async () => {
|
||||||
|
const coordinatePayload = {
|
||||||
|
result: {
|
||||||
|
geographies: {
|
||||||
|
States: [{ NAME: "Texas", STUSAB: "TX", STATE: "48" }],
|
||||||
|
Counties: [{ NAME: "Nueces County", COUNTY: "355", GEOID: "48355" }],
|
||||||
|
"2020 Census Blocks": [{ GEOID: "483550031013005" }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fallbackFetchText = async (url: string): Promise<string> => {
|
||||||
|
if (url.includes("geocoding.geo.census.gov") && url.includes("onelineaddress")) {
|
||||||
|
return JSON.stringify({ result: { addressMatches: [] } });
|
||||||
|
}
|
||||||
|
if (url.includes("nominatim.openstreetmap.org/search")) {
|
||||||
|
return JSON.stringify([
|
||||||
|
{
|
||||||
|
lat: "27.708000",
|
||||||
|
lon: "-97.360000",
|
||||||
|
display_name: "1011 Ennis Joslin Rd Apt 235, Corpus Christi, TX 78412"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (url.includes("geocoding.geo.census.gov") && url.includes("geographies/coordinates")) {
|
||||||
|
return JSON.stringify(coordinatePayload);
|
||||||
|
}
|
||||||
|
if (url.endsWith("/county-directory/")) {
|
||||||
|
return countyIndexHtml;
|
||||||
|
}
|
||||||
|
if (url.endsWith("/county-directory/nueces.php")) {
|
||||||
|
return countyPageHtml;
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected URL: ${url}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const payload = await resolvePublicRecords(
|
||||||
|
"1011 Ennis Joslin Rd APT 235, Corpus Christi, TX 78412",
|
||||||
|
{
|
||||||
|
fetchText: fallbackFetchText
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
payload.matchedAddress,
|
||||||
|
"1011 Ennis Joslin Rd Apt 235, Corpus Christi, TX 78412"
|
||||||
|
);
|
||||||
|
assert.equal(payload.county.name, "Nueces County");
|
||||||
|
assert.equal(payload.state.code, "TX");
|
||||||
|
assert.equal(payload.latitude, 27.708);
|
||||||
|
assert.equal(payload.longitude, -97.36);
|
||||||
|
assert.match(
|
||||||
|
payload.lookupRecommendations.join(" "),
|
||||||
|
/fallback geocoder/i
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolvePublicRecords retries fallback geocoding without the unit suffix", async () => {
|
||||||
|
const seenFallbackQueries: string[] = [];
|
||||||
|
const coordinatePayload = {
|
||||||
|
result: {
|
||||||
|
geographies: {
|
||||||
|
States: [{ NAME: "Texas", STUSAB: "TX", STATE: "48" }],
|
||||||
|
Counties: [{ NAME: "Nueces County", COUNTY: "355", GEOID: "48355" }],
|
||||||
|
"2020 Census Blocks": [{ GEOID: "483550031013005" }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const retryingFetchText = async (url: string): Promise<string> => {
|
||||||
|
if (url.includes("geocoding.geo.census.gov") && url.includes("onelineaddress")) {
|
||||||
|
return JSON.stringify({ result: { addressMatches: [] } });
|
||||||
|
}
|
||||||
|
if (url.includes("nominatim.openstreetmap.org/search")) {
|
||||||
|
const query = new URL(url).searchParams.get("q") || "";
|
||||||
|
seenFallbackQueries.push(query);
|
||||||
|
if (query.includes("APT 235")) {
|
||||||
|
return "[]";
|
||||||
|
}
|
||||||
|
if (query === "1011 Ennis Joslin Rd, Corpus Christi, TX 78412") {
|
||||||
|
return JSON.stringify([
|
||||||
|
{
|
||||||
|
lat: "27.6999080",
|
||||||
|
lon: "-97.3338107",
|
||||||
|
display_name: "Ennis Joslin Road, Corpus Christi, Nueces County, Texas, 78412, United States"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (url.includes("geocoding.geo.census.gov") && url.includes("geographies/coordinates")) {
|
||||||
|
return JSON.stringify(coordinatePayload);
|
||||||
|
}
|
||||||
|
if (url.endsWith("/county-directory/")) {
|
||||||
|
return countyIndexHtml;
|
||||||
|
}
|
||||||
|
if (url.endsWith("/county-directory/nueces.php")) {
|
||||||
|
return countyPageHtml;
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected URL: ${url}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const payload = await resolvePublicRecords(
|
||||||
|
"1011 Ennis Joslin Rd APT 235, Corpus Christi, TX 78412",
|
||||||
|
{
|
||||||
|
fetchText: retryingFetchText
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(seenFallbackQueries, [
|
||||||
|
"1011 Ennis Joslin Rd APT 235, Corpus Christi, TX 78412",
|
||||||
|
"1011 Ennis Joslin Rd, Corpus Christi, TX 78412"
|
||||||
|
]);
|
||||||
|
assert.equal(payload.county.name, "Nueces County");
|
||||||
|
assert.equal(payload.state.code, "TX");
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user