From free text to a parcel
Parse an address, match it to a parcel, and handle every answer the lookup can give.
This guide builds one function: free text in, a decision out. It parses the line, stops early when the parse isn't a street address, looks up the parcel, and turns each lookup status into something your code can act on.
Before you start: an API key in FIELDGEIST_API_KEY. See the Quickstart.
1. Parse
Parse first. The lookup needs a street name and a ZIP or city, and only street addresses have parcels, so the parse tells you whether a lookup is worth paying for.
2. Check the parse
typemust bestreet. PO boxes, rural routes and military addresses have no parcel; a lookup would return422 unsupported_type.- The parse needs
components.streetName, andcomponents.zip5orcomponents.city. Without them a lookup returns400. - Warnings in the "Out of scope" group (
unsupported_intersection,possibly_foreign,empty_input) mean the line isn't one US delivery address.
3. Look up
Send the parse response as is. The lookup reads its type and components.
4. Act on the status
| Check | Then |
|---|---|
unit_required is true | Ask for the unit, or keep the building_id. Check this first: it can come with matched. |
status is matched | Store fieldgeist_id. If fuzzy is true, the street name was corrected; consider confirming. |
status is ambiguous | Show the candidates, or ask for the missing suffix or direction. |
status is no_situs_parcel | Weak match. Review before using. |
status is matched_building | The building matched, not the unit you named. |
status is not_found | Check the input. |
The whole thing
const API = "https://api.fieldgeist.com/v1/verify";
const headers = {
Authorization: `Bearer ${process.env.FIELDGEIST_API_KEY}`,
"Content-Type": "application/json",
};
async function call(path, body) {
const res = await fetch(`${API}${path}`, {
method: "POST",
headers,
body: JSON.stringify(body),
});
const json = await res.json();
if (!res.ok) {
const id = res.headers.get("x-request-id");
throw new Error(`${res.status} ${json.error} (${id ?? "no request id"})`);
}
return json;
}
const OUT_OF_SCOPE = new Set(["unsupported_intersection", "possibly_foreign", "empty_input"]);
export async function resolveAddress(line) {
const parsed = await call("/parse", { line });
if (parsed.type !== "street" || parsed.warnings.some((w) => OUT_OF_SCOPE.has(w.code))) {
return { kind: "not_a_street_address", parsed };
}
const c = parsed.components;
if (!c.streetName || !(c.zip5 || c.city)) {
return { kind: "incomplete", parsed };
}
const match = await call("/lookup", parsed);
if (match.unit_required) {
return { kind: "needs_unit", buildingId: match.building_id, unitCount: match.unit_count, match };
}
switch (match.status) {
case "matched":
return { kind: "matched", fieldgeistId: match.fieldgeist_id, fuzzy: match.fuzzy, match };
case "ambiguous":
return { kind: "ambiguous", candidates: match.candidates };
case "not_found":
return { kind: "not_found" };
default:
// matched_building, no_situs_parcel: usable, but worth a look.
return { kind: "review", match };
}
}Cost
Each address costs a parse and a lookup. Checking the parse before the lookup saves the lookup, the larger of the two, for lines that can't match. Later, refresh a stored parcel with Look up by ID, which costs less than a new lookup. Rates are on the pricing page.
Next steps
- Condos and units: what
unit_requiredmeans and how to ask for a unit. - Confidence and warnings: when to send an answer for review.
- Errors: what to retry.