Sex Offender Search API · v1.0.0

Matching and confidence

The API tells you how each name matched and how the birth date agreed. Build the review queue on those two facts, not on the score alone.

What you will build

A triage function that sorts every returned record into accept (identity confirmed well enough to act on automatically), review (a real candidate a person must look at) or noise (a widened match you choose to drop), using matchDetail.strategies, matchedName.type and matchState. Along the way you pick a match mode for your use case and learn when to widen a search with prefixMatch or nameMatch.

Who does this: anyone screening people by name — admissions, volunteer coordinators, visitor desks, platforms serving them. The result is a queue in which the strong hits are separated from the common-name noise before a reviewer sees them.

Before you start

  • A test key on https://sandbox.offendersearch.app. Sandbox people used here: Exact Match (dob 1980-01-01, one legal-name hit), Alias Match (a registrant whose legal name is different; the hit is through an alias) and Name Only (three same-name candidates, no birth dates).
  • Three match modes on the request: strict, balanced (default), broad. Two finer controls on query: prefixMatch and nameMatch. The full vocabulary is on Matching & confidence; this guide uses it rather than restating it.
  • Two response fields carry the answer: matchDetail.strategies (one key per name field, exact, nickname, initial, prefix, fuzzy, middle or absent, plus alias when an alias produced the hit) and matchState (how the birth date agreed).
The one thing people get wrong. matchConfidence measures how complete the record is, not whether the person at your desk is the registrant. Exact Match scores 0.9 with a dob sent (matchState: "dob_match") and 0.9 with no dob at all (matchState: null). Gate on strategies and matchState; use the score to order the queue.

Step by step

  1. Run the default mode. balanced is the default: exact surname, and nicknames, initials, 3-character prefixes and one-edit typos on the given name, with aliases searched. It is the right mode for a national search; measured against a large competitor corpus it matched 98.28% and broad recovered only 0.11% more.
  2. See an alias hit, then switch to strict. Search Alias Match in balanced and again with "match": "strict". The alias hit disappears in strict, which searches the registered legal name only.
  3. Widen deliberately. Use prefixMatch for a partial name (minimum 3 characters; it forces broad) or nameMatch to switch individual strategies on per field. Widening is a choice you make per request, never a default.
  4. Triage. Implement the function below and feed it records[]. Send review items to a person with the photo, age, address and sources[].recordUrl.

Step 2 — the alias hit in balanced, gone in strict

POST /v1/search body (balanced is the default)
{ "query": { "firstName": "Alias", "lastName": "Match" } }
Response record (trimmed)
{
  "recordId": "rec_ca28e3ec410c9ee37d7d",
  "personRef": "prs_ca28e3ec410c9ee37d7d",
  "matchConfidence": 0.55,
  "matchBasis": ["lastName", "firstName", "dob", "lastName:exact", "firstName:exact", "alias:exact", "alias_match"],
  "matchDetail": {
    "strategies": { "lastName": "exact", "firstName": "exact", "alias": "exact" },
    "fieldsPresent": ["lastName", "firstName", "dob"],
    "signals": ["matchedAlias"],
    "matchedNameType": "alias",
    "verification": null
  },
  "matchedName": { "value": "Alias Match", "type": "alias" },
  "name": { "first": "Jordan", "middle": "Sample", "last": "Placeholder", "suffix": "", "full": "Jordan Sample Placeholder" },
  "aliases": ["Alias Match", "J. Placeholder"],
  "dob": "1977-07-07", "dobPrecision": "exact",
  "matchState": null, "unverified": false
}

The registered name is Jordan Sample Placeholder; the name you sent is one of the aliases. matchedName.type is alias, strategies.alias is exact, and the score is capped at 0.55 — the lowest the engine produces — because an alias-only hit is never presented as certainty. The same request with "match": "strict" returns counts.records: 0.

Step 3 — widening on purpose

{ "query": { "firstName": "Jordan", "lastName": "Riv", "prefixMatch": "lastName" } }
// 3+ characters required (a shorter prefix is a 422); forces match=broad;
// an exact hit always ranks above a prefix hit; prefix hits cap at 0.60.

Step 4 — the triage function

STRONG_DOB = {"dob_match"}
WEAK_DOB = {"year_match", "age_match", "no_dob_age_year", None}

def triage(record, queried=("lastName", "firstName")):
    """accept | review | noise for one record of POST /v1/search."""
    strategies = record["matchDetail"]["strategies"]
    legal = record["matchDetail"]["matchedNameType"] == "legal"
    all_exact = all(strategies.get(f) == "exact" for f in queried)
    if all_exact and legal and record["matchState"] in STRONG_DOB:
        return "accept"          # exact legal name + confirmed full date of birth
    if record["matchState"] == "age_mismatch":
        return "noise"           # only present when you sent onAgeMismatch: "flag"
    if strategies.get("lastName") == "fuzzy" and record["matchState"] in WEAK_DOB:
        return "noise"           # broad-mode surname typo with nothing confirming it
    return "review"              # nickname / initial / prefix / alias / middle, or a weak date

def queue(result):
    rows = sorted(result["records"], key=lambda r: -(r["matchConfidence"] or 0))
    return {"accept": [r for r in rows if triage(r) == "accept"],
            "review": [r for r in rows if triage(r) == "review"],
            "incomplete": result["status"] != "complete"}

Run it on the three sandbox people: Exact Match with dob lands in accept; Alias Match lands in review (alias, no date sent); each Name Only record lands in review (no_dob_age_year). "Accept" here means the identity evidence is strong enough for your automatic path — it is still a registry match to be confirmed against the source page, not a verdict about the person.

Reading the result

FieldValuesWhat it means for the queue
matchDetail.strategies.lastNameexact, prefix, fuzzyexact is the only surname strategy in strict and balanced; prefix/fuzzy appear only in broad and cap the score at 0.60.
matchDetail.strategies.firstNameexact, nickname, initial, prefix, fuzzy, middle, absentexact has no cap; nickname caps at 0.75; the rest at 0.60. absent means the registry publishes no given name — kept, never a confirmation.
matchDetail.strategies.aliasexact, prefix, fuzzy (present only on an alias hit)An alias produced the hit; matchedName.type is alias; cap 0.55. Always review.
matchDetail.matchedNameTypelegal, aliasWhich name matched. Auto-accept only legal.
matchStatedob_match, year_match, age_match, no_dob_age_year, age_mismatch, nullA confirmed full date lifts every cap; a confirmed year raises the floor to 0.80. null means you sent no dob/age.
matchConfidence0–1, or null on a nameless searchOrder the queue with it. Several strategies share a ceiling, so it cannot tell them apart.
matchBasis[]mixed tokens, e.g. lastName:exact, alias:prefix, a bare dobA flat explanation for a reviewer. Prefer matchDetail in code.

Strategy ranks, ceilings and the exact rules behind each mode are in Matching & confidence. The birth-date side of the decision has its own guide: Date-of-birth matching.

Edge cases and pitfalls

  • Typos in `match` are refused, not widened: "match": "strick" answers 422 with Unknown match 'strick'. Allowed: balanced, broad, strict.
  • `broad` fuzzes the surname and returns roughly 13.5% surname false positives by design, every one labelled lastName:fuzzy in matchBasis. It also reads far more candidate rows and can come back partial at national scope; scope it with jurisdictions or locationScoped.
  • Prefix hits need 3 characters. {"lastName": "Ma", "prefixMatch": "lastName"} is a 422: prefixMatch prefix matching on lastName requires at least 3 characters.
  • A common surname with no given name or date can hit the per-registry candidate cap and come back partial with incompleteReason: "truncated". Add a firstName, dob or age rather than retrying.
  • Compound surnames match on one token: a Smith query matches Hamilton-Smith as exact, never Goldsmith.
  • No phonetic matching. Smyth → Smith works because it is one edit; Schmidt → Smith does not. Send a second query for a variant you need.
  • A missing `strategies` key for a field you queried means that field did not drive the match; treat it as review, as the triage function does.

Production checklist

  • Mode chosen per use case and sent explicitly (balanced for screening; strict where you hold the exact legal name; broad only scoped).
  • Auto-accept requires strategies all exact, matchedNameType: "legal" and matchState: "dob_match"; everything else is reviewed by a person.
  • Reviewers see matchedName, aliases[], images[], age, addresses[] and sources[].recordUrl, and record verdicts as clearances.
  • The queue is ordered by matchConfidence but never gated by it alone.

Product overview