Docs

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

  • type must be street. PO boxes, rural routes and military addresses have no parcel; a lookup would return 422 unsupported_type.
  • The parse needs components.streetName, and components.zip5 or components.city. Without them a lookup returns 400.
  • 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

CheckThen
unit_required is trueAsk for the unit, or keep the building_id. Check this first: it can come with matched.
status is matchedStore fieldgeist_id. If fuzzy is true, the street name was corrected; consider confirming.
status is ambiguousShow the candidates, or ask for the missing suffix or direction.
status is no_situs_parcelWeak match. Review before using.
status is matched_buildingThe building matched, not the unit you named.
status is not_foundCheck 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

On this page