# From free text to a parcel (/guides/parse-then-match)

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](/quickstart).

## 1. Parse [#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 [#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 [#3-look-up]

Send the parse response as is. The lookup reads its `type` and `components`.

## 4. Act on the status [#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 [#the-whole-thing]

<Tabs items={["JavaScript", "Python"]}>
<Tab value="JavaScript">

```js
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 };
  }
}
```

</Tab>
<Tab value="Python">

```python
import os
import requests

API = "https://api.fieldgeist.com/v1/verify"
HEADERS = {"Authorization": f"Bearer {os.environ['FIELDGEIST_API_KEY']}"}
OUT_OF_SCOPE = {"unsupported_intersection", "possibly_foreign", "empty_input"}


def call(path, body):
    res = requests.post(f"{API}{path}", headers=HEADERS, json=body, timeout=10)
    data = res.json()
    if not res.ok:
        request_id = res.headers.get("x-request-id", "no request id")
        raise RuntimeError(f"{res.status_code} {data.get('error')} ({request_id})")
    return data


def resolve_address(line):
    parsed = call("/parse", {"line": line})

    if parsed["type"] != "street" or any(w["code"] in OUT_OF_SCOPE for w in parsed["warnings"]):
        return {"kind": "not_a_street_address", "parsed": parsed}
    c = parsed["components"]
    if not c.get("streetName") or not (c.get("zip5") or c.get("city")):
        return {"kind": "incomplete", "parsed": parsed}

    match = call("/lookup", parsed)

    if match.get("unit_required"):
        return {
            "kind": "needs_unit",
            "building_id": match["building_id"],
            "unit_count": match["unit_count"],
            "match": match,
        }
    status = match["status"]
    if status == "matched":
        return {"kind": "matched", "fieldgeist_id": match["fieldgeist_id"], "fuzzy": match["fuzzy"]}
    if status == "ambiguous":
        return {"kind": "ambiguous", "candidates": match["candidates"]}
    if status == "not_found":
        return {"kind": "not_found"}
    # matched_building, no_situs_parcel: usable, but worth a look.
    return {"kind": "review", "match": match}
```

</Tab>
</Tabs>

## Cost [#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](/api-reference/lookup-by-id), which costs less than a new lookup. Rates are on the [pricing page](https://fieldgeist.com/pricing).

## Next steps [#next-steps]

- [Condos and units](/concepts/condos-and-units): what `unit_required` means and how to ask for a unit.
- [Confidence and warnings](/concepts/confidence-and-warnings): when to send an answer for review.
- [Errors](/api-reference/errors): what to retry.
