# Matching, confidence & partial names

> strict, balanced and broad match modes, the closed match-strategy vocabulary, confidence ceilings, nickname expansion, and prefix search on partial names.

- **HTML:** https://offendersearch.app/docs/matching
- **Base URL:** https://api.offendersearch.app
- **Authentication:** `X-API-Key` request header
- **OpenAPI:** https://offendersearch.app/openapi.json · https://offendersearch.app/openapi.yaml
- **All documentation as markdown:** https://offendersearch.app/docs.md

## Match modes

The `match` field sets the fuzzy-match tolerance, applied uniformly across every jurisdiction, so behaviour is consistent no matter which states you hit. Whatever mode you choose, supplying a `dob` or `age` is the strongest lever on accuracy.

| Mode | First name | Last name | Use it for |
| --- | --- | --- | --- |
| `strict` | Exact only | Exact only | Lowest false positives; pair with a DOB. |
| `balanced` | Nicknames, variants, initials, typos (edit-distance 1) | Exact | The default — good recall with controlled noise. |
| `broad` | All balanced rules | Also prefix + typo tolerance | Maximum recall; expect more low-confidence rows to triage. |

Read `matchDetail` to see exactly why a record matched — it splits the match into `strategies` (for example `{"lastName":"exact"}`), the `fieldsPresent` on the record, and any `signals`. The flat `matchBasis` array carries the same information mixed together.

**Nickname-aware name search.** Under `balanced` and `broad`, a `firstName` is expanded to its common nicknames and variants before matching, and the expansion used is echoed back on every record in the output-only `nicknames[]` array. It is computed at serialize time and is never presented as data published by a jurisdiction.

## The match-strategy vocabulary — the complete list

Every record tells you *how* it matched, per field, in `matchDetail.strategies` (and, in the older flat form, as the `field:strategy` tokens of `matchBasis`). **The list is closed.** Any token the engine emits that this mapping does not define arrives under `matchDetail.other` rather than appearing here.

| Strategy | Can appear on | Meaning | Rank | Ceiling | Turned on by |
| --- | --- | --- | --- | --- | --- |
| `exact` | lastName, firstName, alias | The field equals what you sent — or equals one whole token of a compound surname, so a Smith query matches Hamilton-Smith. | 1 | none | Every mode. Cannot be switched off. |
| `nickname` | firstName only | A known given-name equivalence. Never appears on a surname — there is no surname nickname table, and asking for one is a 422. | 2 | 0.75 | balanced, broad |
| `initial` | firstName only | A single letter against a full given name, in either direction. | 2 | 0.60 | balanced, broad. Not requestable via nameMatch. |
| `prefix` | lastName, firstName, alias | The field starts with what you sent. Minimum 3 characters, anchored at a token start — never a mid-word substring. | 3 | 0.60 | firstName: balanced, broad · lastName: broad only |
| `fuzzy` | lastName, firstName, alias | A spelling variant or typo within an edit-distance budget that scales with name length. | 4 | 0.60 | firstName: balanced, broad · lastName: broad only |
| `middle` | firstName only | The given name you sent is the person’s MIDDLE name, matched as a whole token of the registered legal name — 81% of records carry one, and many people go by theirs. Whole token and exact only: minimum 2 characters, never a prefix, never fuzzy, and an initial is not a name in either direction. Tried only after the registered first name and every alias have failed, so a stronger basis is always the one reported. matchedNameType stays "legal"; signals also carries matchedMiddleName. | 5 | 0.60 | balanced, broad |
| `absent` | firstName only | The record publishes no first name at all, so your query could not be DISPROVED. The record is kept and flagged — it is never a confirmation. | 6 | 0.60 | Every mode. Not requestable via nameMatch. |

**Rank** is the ordering contract, not a score: `1` is the strongest evidence, and results sort by surname rank then given-name rank, with legal-name hits ahead of alias-only hits. **Ceiling** is the cap the strategy puts on `matchConfidence`, applied automatically so a widened hit is never reported as certainty. The cap is lifted in exactly one case: you supplied a `dob` and the record’s full date of birth matched it.

**Choosing an auto-accept threshold.** Gate on `matchDetail.strategies`, not on `matchConfidence` alone — several strategies share one ceiling. Auto-accept only `exact` on every field you queried with `matchedNameType: "legal"`, and for anything consequential require a confirmed date of birth as well. Send `nickname`, `initial`, `prefix` and `fuzzy` hits to human review. Never auto-accept an alias-only hit or `firstName: "absent"` — the latter means *this person could not be ruled out*, which is not the same fact as *this is your person*.

## Partial name search

Set `prefixMatch` and the name you send is treated as the **start** of a name. Searching `thom` returns Thomas, Thompson and Thomason — on the first name, the last name, or both — matched against every recorded alias as well as the registered name.

```json
{
  "query": {
    "firstName": "thom",
    "lastName": "and",
    "prefixMatch": "both"
  }
}
```

`prefixMatch` accepts `"firstName"`, `"lastName"`, `"both"`, or a list. When both fields are prefix-matched, a record must satisfy *both*.

| Rule | What it means |
| --- | --- |
| Prefix, not substring | thom matches Thompson but not Bothomley. We anchor at the start of a name, never mid-word. |
| Minimum 3 characters | A shorter prefix is rejected with 422. One or two letters is a bulk download, not a search. |
| Every name part | A compound or hyphenated surname matches on any of its parts: thom finds Hamilton-Thompson. |
| Aliases included | Prefixes are matched against every recorded alias as well as the registered name, so a variation of a first name still finds the person. matchedName tells you which one matched. |
| A superset of exact | Turning prefix matching on never loses a record that an exact search would have returned. |
| Exact ranks first | An exact surname match always appears above a prefix match, and the order is deterministic — paging never reshuffles results. |
| Scope it when you can | A prefix search scoped with an explicit jurisdictions list returns in roughly 30 ms; an unscoped nationwide prefix search on a very common stem returns in around 0.6 s. Note that query.state does NOT scope the search — it is a residence filter, and every jurisdiction is still queried. |

### Per-field control with nameMatch

`prefixMatch` is the shorthand. Send `nameMatch` to drive matching yourself and list exactly the strategies you want per field. Anything you leave out is **off** for that field, and `nameMatch` overrides both `prefixMatch` and `match`.

```json
{
  "query": {
    "firstName": "Robert",
    "lastName": "thom",
    "nameMatch": {
      "firstName": [],
      "lastName": ["prefix"],
      "aliases": true
    }
  }
}
```

| Strategy | Applies to | Meaning | Example |
| --- | --- | --- | --- |
| `exact` | firstName, lastName | The name equals what you sent. Always on — it cannot be switched off. | `Thomas → Thomas` |
| `prefix` | firstName, lastName | The name starts with what you sent. Minimum 3 characters. | `thom → Thomas, Thompson, Thomason` |
| `nickname` | firstName | A known given-name equivalence. | `bob → Robert` |
| `fuzzy` | firstName, lastName | A spelling variant or typo within edit distance. | `smyth → Smith` |

### Which name matched — legal or alias

A prefix is matched against every recorded alias as well as the registered name. Every record tells you which name did it.

```json
{
  "name":        { "first": "Ryan", "last": "Lloyd" },
  "aliases":     ["BRANDON THOMPSON"],
  "matchedName": { "value": "BRANDON THOMPSON", "type": "alias" },
  "matchBasis":  ["lastName:prefix", "alias:prefix", "alias_match"],
  "matchConfidence": 0.55
}
```

Send `"nameMatch": { "aliases": false }` to search registered names only.

### How results are ordered

The ordering is deterministic, so paging never reshuffles. In order:

1. Surname match strength — exact, then nickname, then prefix, then fuzzy
2. First-name match strength, on the same scale
3. Legal-name matches before alias-only matches
4. matchConfidence, highest first
5. Last name, first name, then date of birth
6. Record id — the final tie-break, so the ordering is total and repeatable

### Confidence ceilings

Widening a search widens the risk of returning the wrong person, so a partial-name hit is never presented as certainty.

| How the name matched | matchConfidence ceiling |
| --- | --- |
| Exact match on the registered name | `No cap` |
| Nickname | `0.75` |
| Prefix, fuzzy, or no first name published | `0.60` |
| Alias only | `0.55` |

The one exception: if you supplied a `dob` and the record’s full date of birth matches it, the cap is lifted. Supplying a `dob` or `age` alongside a prefix search is the single best way to keep precision while widening recall.

---

## Related

- Previous: [Search — POST /v1/search](https://offendersearch.app/docs/search.md)
- Next: [Searching by date of birth](https://offendersearch.app/docs/date-of-birth.md)
- Index: [Offendersearch API documentation](https://offendersearch.app/docs.md)
