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_entitiesis 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 toq="smith"&state=FLq="hayden WA"→ equivalent toq="hayden"&state=WAq="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/sourcewhen the user explicitly names a state/county. - Pass
lat/lonwhen 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
bboxand highlight the member parcels viasetFeatureStateon 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 map —
setFeatureState({ source, sourceLayer, id })for each owner-group member. RequirespromoteId: "id"on the PMTiles parcels source layer (small CDK + tile-build change). - Entity-graph hits — once
sos_entitiesis populated, search surfaces business entities alongside parcels and ranks them with the same scoring rules. - Geocoding tool — a
parcelpump_geocode_placeMCP 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.