parcelpump
Sign in

Display

DOCS / SEARCH

Search

Fuzzy parcel + entity search at GET /v1/search. Modeled on Google Maps' principles: location is a signal, never a filter; textual specificity outranks proximity; the consumer can override at any point with explicit terms.

What you can search by

  • Owner name — full or partial. "HAYDEN FARMS INC", "hayden", "haydn" all match.
  • Address — situs street + city. "3750 Cottonwood", "PASCO 99301".
  • Owner mailing address — useful when the owner lives out-of-state.
  • Parcel ID — exact match scores +5.0 (so a parcel-ID-looking query like "123290284" surfaces only that parcel at the top).
  • Entity (SoS) name — once sos_entities is populated.

The query is run against pre-computed pg_trgm indexes on each column. At 7M projected parcels, p99 latency stays under 100ms.

How relevance is scored

Each candidate row gets a base score from the best-matching column's word_similarity × column weight, plus bonuses:

Signal Score contribution
Owner-name word match × 3.0
Situs-address word match × 2.0
Situs-city word match × 1.5
Owner-mailing word match × 1.0
source_parcel_id exact + 5.0
source_parcel_id prefix + 1.0
Owner-name left-prefix + 0.5
Situs-address left-prefix + 0.3
Proximity bonus (when lat/lon supplied) up to + 1.5

The proximity bonus decays linearly from +1.5 at distance 0 to 0 at proximity_km (default 50 km). Distant rows still match — they just rank lower.

Endpoint

GET /v1/search?q=<text>
              &limit=<n>          (default 20, cap 100)
              &types=parcels,entities    (default: both)
              &state=<XX>          (strict 2-letter filter)
              &source=<slug>       (strict county filter)
              &lat=<deg>           (proximity-rank anchor)
              &lon=<deg>
              &proximity_km=<km>   (default 50)

state and source are strict filters — when set, distant matches are excluded entirely. lat/lon is a bias only — set it to re-rank by closeness without filtering anything out.

Query-term parsing

If you don't supply state, the server extracts a trailing state token from q. So:

  • q="smith florida" → equivalent to q="smith"&state=FL
  • q="hayden WA" → equivalent to q="hayden"&state=WA
  • q="jackass mountain" → no state extraction (no trailing state token)

Server-side parsing is a convenience; passing state explicitly is always cleaner.

Response shape

{
  "count": 14,
  "results": [
    {
      "kind": "parcel",
      "source": "franklin-county-wa",
      "source_parcel_id": "123300017",
      "state": "WA",
      "county_name": "Franklin",
      "owner_name": "HAYDEN FARMS INC",
      "situs_address": "3750 COTTONWOOD DR, PASCO 99301",
      "situs_city": "PASCO",
      "situs_state": "WA",
      "assessed_value": 542300,
      "lat": 46.4106,
      "lon": -119.2274,
      "score": 3.50
    },
    {
      "kind": "entity",
      "id": "uuid",
      "jurisdiction": "WA",
      "registry_number": "604123456",
      "name": "HAYDEN FARMS, INC.",
      "entity_type": "INC",
      "status": "ACTIVE",
      "registered_agent_name": "JOHN HAYDEN",
      "linked_parcel_count": 8,
      "score": 1.55
    }
  ]
}

lat/lon on parcel hits is the geometry centroid in EPSG:4326. Useful for flyTo on a map. Null for parcels with no geometry yet.

Examples

Plain text — pure textual ranking

curl -H "X-Parcelpump-Key: $K" \
  'https://api.parcelpump.io/v1/search?q=hayden'

Returns top hits across all states. A user in WA looking for a person in FL gets the FL match if the name is unique enough to rank.

Strict county filter

curl -H "X-Parcelpump-Key: $K" \
  'https://api.parcelpump.io/v1/search?q=jackass&source=franklin-county-wa'

Only Franklin Co. WA results.

Proximity bias (Google-Maps-style)

curl -H "X-Parcelpump-Key: $K" \
  'https://api.parcelpump.io/v1/search?q=smith+farm&lat=46.24&lon=-119.10'

Re-ranks by closeness to (46.24N, 119.10W) — Pasco, WA. Distant matches still surface (re-rank, not filter).

Parcel-ID lookup

curl -H "X-Parcelpump-Key: $K" \
  'https://api.parcelpump.io/v1/search?q=123290284'

Exact match scores +5.0; surfaces only that parcel.

Trailing state name in q (parsing convenience)

curl -H "X-Parcelpump-Key: $K" \
  'https://api.parcelpump.io/v1/search?q=smith+washington'
# Equivalent to:
curl -H "X-Parcelpump-Key: $K" \
  'https://api.parcelpump.io/v1/search?q=smith&state=WA'

How LLMs should use it (via MCP)

The parcelpump_search MCP tool exposes the same parameters. Tool description (in mcp-server/src/index.ts) tells the LLM:

  • Pass state/source when the user explicitly names a state/county.
  • Pass lat/lon when the user implies "near here" (e.g., "find ranches near my farm in Pasco" → look up Pasco coords → pass them).
  • Default: just q — let the textual ranking decide.
  • Don't over-constrain: a unique-named search like "Hayden Farms" needs no filters.

The LLM can resolve common place names to lat/lon from its training data without a separate geocoding tool. For unfamiliar places, a future parcelpump_geocode_place tool will handle lookup.

Performance budget

  • p50 latency at current scale (110K parcels): ~30-60ms warm.
  • p99 at projected scale (7M parcels): under 100ms — the trigram GIN filter narrows candidates to thousands before ranking + distance math.
  • Map-side viewport-driven bias: passing the map's getCenter() on every keystroke is fine; the server-side query is cheap and the client is debounced 250ms.

Owner grouping (?group=owner)

When ?group=owner is set, parcels with the same owner_name collapse into a single result of kind: "owner-group":

{
  "kind": "owner-group",
  "owner_name": "HAYDEN AG INC",
  "parcel_count": 8,
  "states": ["WA"],
  "score": 3.50,
  "bbox": { "west": -119.23, "south": 46.40, "east": -119.21, "north": 46.42 },
  "members": [
    { "source": "franklin-county-wa", "source_parcel_id": "...",
      "situs_address": "...", "assessed_value": 542300, "lat": 46.41, "lon": -119.22 }
  ]
}

Single-member rows stay as kind: "parcel" even with grouping enabled (no point in a one-member "group"). Mixing in kind: "entity" from sos_entities continues normally.

UX recipe (Google-Maps-style):

  • Render group rows distinctly in the dropdown (e.g. "GRP" badge).
  • On click: fly the map to the group's bbox and highlight the member parcels via setFeatureState on the PMTiles parcels source.
  • On click of a single-member row: existing per-parcel flow.

Zoning filter (?zoning=)

Prefix-matches three columns: zoning_code, zoning_description, land_use_class. Strict filter — non-matching parcels drop out.

# All parcels coded agricultural anywhere in our data
curl -H "X-Parcelpump-Key: $K" \
  'https://api.parcelpump.io/v1/search?q=jackass&zoning=agricultural'

# Specific code prefix
curl -H "X-Parcelpump-Key: $K" \
  'https://api.parcelpump.io/v1/search?q=acme&zoning=R-1'

Future enhancements

  • Per-member highlight on mapsetFeatureState({ source, sourceLayer, id }) for each owner-group member. Requires promoteId: "id" on the PMTiles parcels source layer (small CDK + tile-build change).
  • Entity-graph hits — once sos_entities is populated, search surfaces business entities alongside parcels and ranks them with the same scoring rules.
  • Geocoding tool — a parcelpump_geocode_place MCP tool for unfamiliar place names. Optional; LLMs handle most lookups inline.
  • "Near me" via browser geolocation — opt-in UI on the dashboard map; one-time prompt, cached.