---
title: "Search Properties"
method: GET
path: "/properties/search"
tags: ["Properties", "Properties"]
---

# Search Properties

`GET /properties/search`

**Beta.** Query parameters, response fields, and the absence-trust surface may still change in response to how the endpoint is used in practice. Treat the shape as unstable while it is in beta. Returns properties within a geo scope (state, county, city, ZIP, ZIP+4, or address) and/or owned by named `legal_owner`s, most recently permitted first. At least one of `geo_id` and `legal_owner` is required; a `legal_owner` search needs no geo scope. Pass `cursor` from a page's `next_cursor` to page through results. Filter by `permit_tags` (presence and `-`-prefixed exclusion), `permit_status`, `permit_from`, and `property_type`. Properties with no attributed type are excluded; property attributes are available for 70.1% of properties. An absence filter (a `-`-prefixed tag) returns properties with no resolved permit of the excluded type in covered, arrived data: a permit that never linked to an address, or one that has not yet arrived within a jurisdiction's ingestion lag, is not counted against the claim. Every absence-class row carries a `trust` object stating three honesty terms — the jurisdiction's permit `coverage_tier`, its `unresolved_rate` (the share of its permits that never linked to an address), and the row's `data_horizon` — plus `horizon_basis` (whether that horizon is measured, pooled, or a state prior) and `footprint_basis` (whether suppression could resolve the row's geo); the response `trust_summary` row-weights them across the page. A presence-only search omits both. Freshness: /properties is served from a whole-table rebuild swapped in by a blue/green cutover — the rebuild is the unit of refresh, and the absence-trust metadata is rebuilt from the same data it describes. An absence claim's staleness bound is the rebuild cadence plus each jurisdiction's permit-ingestion lag: permits reach the data a median 84 days (p90 188 days) after their start_date, so a recent 'no permit since D' stays under-observed until that lag elapses. Each row's `data_horizon` is the per-row handle on that bound — the most recent start_date past which 'no X since D' is under-observed for that row. Not supported, each for one reason: `permit_to` or any upper date bound — a max-date rollup cannot bound a closed date window without dropping addresses whose latest permit postdates it (use /permits/search); 'before D' absence — a max-date cannot tell a history that also holds later permits from one that does not; closed-window absence within [D1, D2] — only 'ever' and 'since D' are expressible on max-date maps; 'has X since D and no Y since D' in one query — a mixed query binds an exclusion to 'ever', so per-predicate dates cannot be composed; and jurisdiction geo_ids as a scope.

## Query parameters

- `geo_id` string, nullable — Scope: a 2-letter state, 5-digit ZIP, ZIP+4, or a Shovels state/county/city/address geolocation id. Jurisdiction ids are not accepted. Optional when `legal_owner` is given, required otherwise.
- `legal_owner` string[], nullable — Filter by the property's legal owner, matched on the owner's canonical form — case and spacing variants of one name return the same properties. Pass up to 10 `legal_owner` params to span several owners. Without a `geo_id` this searches an owner's properties nationwide; with one it narrows that scope to the owner. A name the data uses for an unidentified owner (for example 'RECORD OWNER') is rejected.
- `permit_tags` string[], nullable — Filter by canonical permit tags. Repeat the key for several tags (e.g. `?permit_tags=roofing&permit_tags=-solar`). A positive tag keeps properties with that tag on some permit; a `-` prefix excludes the tag (properties without it). Multiple positive tags require each tag (address grain — a documented divergence from /permits/search, where they must share one permit). With `permit_status`, each positive tag must be matched by a permit that also has one of the statuses. With `permit_from`: a positive tag matches since the date; an exclusion means 'no such tag since the date' in a pure-absence query, or 'never' when combined with any positive filter.
- `permit_status` string[], nullable — Filter by permit status; repeat the key for several (final, in_review, inactive, active). With positive `permit_tags` this is a same-permit conjunction; on its own it keeps properties with a permit of one of the statuses. 'unknown' is not a filterable status.
- `permit_from` string, date, nullable — Bind the tag/status/absence filters to this ISO date (YYYY-MM-DD): a positive filter matches since the date, a pure-absence exclusion means 'none since the date'. With no tag/status filter it keeps properties whose most recent permit is on or after the date.
- `permit_to` string, nullable — Not supported on /properties/search — use /permits/search for a date-window search.
- `property_type` string[], nullable — Filter by property type: residential, commercial, industrial, agricultural, vacant land, exempt, miscellaneous, office, recreational. Repeat the key to match any of several types (e.g. `?property_type=residential&property_type=commercial`). Properties with no attributed type are excluded; property attributes are available for 70.1% of properties.
- `permit_tags_unfinaled` string[], nullable — Keep properties with an UNFINALED permit of each named canonical tag. Repeat the key for several (e.g. `?permit_tags_unfinaled=solar&permit_tags_unfinaled=roofing`). Permit status — not a permit's final_date — is the finaled flag: a final permit carries a NULL final_date 14.7% of the time, and ~41% of permits with a NULL final_date are in fact finaled by status, so a `final_date IS NULL` test over-counts unfinaled permits and is wrong ~41% of the time. A tag is unfinaled for an address only when its latest non-final permit of that tag is more recent than its latest final permit of that tag (equal dates count as finaled); this clear rule removes the false positives a plain 'any non-final permit of this tag' test would keep — for solar, 31.5% of them, though the share varies by tag (nearer 13-18% for tags such as roofing and hvac). A permit whose status is unknown (NULL) counts as neither final nor unfinaled. With `permit_from`, the tag's latest unfinaled permit must be on or after the date.
- `property_min_market_value` integer, nullable — Minimum assessed market value in integer cents (dollars x 100). A property with no assessed market value never matches a range filter (a value is available for 61.0% of properties).
- `property_max_market_value` integer, nullable — Maximum assessed market value in integer cents (dollars x 100). A property with no assessed market value never matches a range filter (a value is available for 61.0% of properties).
- `property_min_lot_size` integer, nullable — Minimum lot size in square feet. A property with no lot size never matches a range filter (a value is available for 68.7% of properties).
- `property_max_lot_size` integer, nullable — Maximum lot size in square feet. A property with no lot size never matches a range filter (a value is available for 68.7% of properties).
- `property_min_building_area` integer, nullable — Minimum building area in square feet. A property with no building area never matches a range filter (a value is available for 60.8% of properties).
- `property_max_building_area` integer, nullable — Maximum building area in square feet. A property with no building area never matches a range filter (a value is available for 60.8% of properties).
- `property_min_unit_count` integer, nullable — Minimum unit count. A property with no unit count never matches a range filter (a value is available for 70.1% of properties).
- `property_max_unit_count` integer, nullable — Maximum unit count. A property with no unit count never matches a range filter (a value is available for 70.1% of properties).
- `property_min_year_built` integer, nullable — Minimum year built. A property with no year built never matches a range filter (a value is available for 61.1% of properties).
- `property_max_year_built` integer, nullable — Maximum year built. A property with no year built never matches a range filter (a value is available for 61.1% of properties).
- `cursor` string, nullable — Opaque keyset cursor from a previous page's next_cursor.
- `size` integer — Page size (1-100).
- `include_total_count` boolean — When true and on the first page (no cursor), include a total_count capped at 10,000.

## Response `200`

A page of properties in the scope.

- PaginatedPropertiesResponse — Paginated /properties response; carries the honesty summary on absence pages.
  - `items` PropertiesRead[], required — The list of items returned in the response following given criteria.
    - `id` string, required — The base64 address handle; the property's public id.
    - `street_no` string, nullable — Street number of the address.
    - `street` string, nullable — Street name of the address.
    - `city` string, nullable — City of the address.
    - `city_id` string, nullable — Base64 city handle.
    - `zip_code` string, nullable — ZIP code of the address.
    - `zip_code_ext` string, nullable — ZIP+4 extension of the address.
    - `county` string, nullable — County of the address.
    - `county_id` string, nullable — Base64 county handle.
    - `state` string, nullable — State of the address.
    - `lat` number, nullable — Latitude of the address.
    - `long` number, nullable — Longitude of the address.
    - `permit_count` integer — Count of permits linked to the address.
    - `untagged_permit_count` integer — Count of the address's permits carrying no canonical tag.
    - `total_job_value` integer — Sum of permit job values in integer cents (dollars x 100).
    - `contractor_count` integer — Distinct contractors across the address's permits.
    - `tags` string[] — Distinct canonical tags present on the address's permits.
    - `tag_status_pairs` string[] — Distinct tag:status pairs (status folded to 'unknown' when NULL).
    - `statuses` string[] — Distinct permit statuses incl the 'unknown' NULL fold.
    - `tag_tally` object — Map tag -> permit count.
    - `last_date_by_tag` object — Map tag -> latest permit start_date.
    - `last_unfinaled_date_by_tag` object — Map tag -> latest non-final start_date under the clear rule.
    - `last_date_by_pair` object — Map tag:status -> latest permit start_date.
    - `last_date_by_status` object — Map status -> latest permit start_date.
    - `last_permit_date` string, date, nullable — Latest permit start_date across all of the address's permits; null when never permitted.
    - `apn` string, nullable — Assessor parcel number.
    - `property_type` string, nullable — Property type.
    - `property_type_detail` string, nullable — Property type detail.
    - `year_built` integer, nullable — Year the property was built.
    - `lot_size` integer, nullable — Lot size of the property.
    - `story_count` integer, nullable — Number of stories in the property.
    - `unit_count` integer, nullable — Number of units in the property.
    - `building_area` integer, nullable — Building area of the property.
    - `assess_market_value` integer, nullable — Assessed market value in integer cents (dollars x 100).
    - `owner_type` string, nullable — Type of property owner.
    - `legal_owner` string, nullable — Legal owner of the property.
    - `trust` PropertyTrust — Per-row absence-honesty surface for one property in an absence-class response. Present and non-null on every row of an absence or mixed-exclusion response, absent on presence-only responses (optional-by-polarity, wired at the route). Every field is required: a row whose (jurisdiction, tag) trust lookup misses carries the conservative-fallback instance rather than a null object.
      - `unresolved_rate` number, required — Share of the row jurisdiction's permits of the tag that never linked to an address.
      - `coverage_tier` 'high' | 'medium' | 'low', required — Coverage bucket for the row's jurisdiction (90/50 cutoffs).
      - `data_horizon` string, date, required — Most recent start_date past which 'no X since D' is under-observed; never null.
      - `horizon_basis` 'measured' | 'pooled' | 'prior', required — How data_horizon was estimated: a measured cohort, a pooled fallback, or the state prior.
      - `trust_jurisdiction_basis` 'own' | 'dominant' | 'unknown', required — Whether the trust join used the row's own jurisdiction, its ZIP's dominant one, or none.
      - `trust_jurisdiction_error_bar` number — Measured error rate of the trust jurisdiction: the 6.13% ZIP-dominant estimate error on a 'dominant' basis, 0 on an 'own' or 'unknown' basis.
      - `footprint_basis` 'matched' | 'unknown', required — Whether footprint suppression could resolve the row's geo scope.
      - `flags` string[] — Row-grain honesty flags, e.g. since_d_beyond_horizon or trust_row_missing.
  - `size` integer, required — The number of items returned in the response.
  - `next_cursor` string, nullable, required — The cursor for retrieving the next page of results.
  - `total_count` TotalCount — Capped result count with Elasticsearch-style {value, relation} shape. When the exact count is known and within the cap, relation is "eq" and value is that exact count. When the count exceeds the cap, relation is "gte" and value is the cap the count was probed against, meaning "the actual count is at least value". The cap is COUNT_CAP for every wire-facing endpoint; internal guard paths probe against their own cap, so value carries whatever cap produced it.
    - `value` integer, required — The count value; capped at the probe's cap (10,000 on the wire).
    - `relation` 'eq' | 'gte', required — "eq" means value is the exact count. "gte" means the actual count is at least value (the cap).
  - `trust_summary` TrustSummary — Response-level, row-weighted absence-honesty summary for an absence-class page.
    - `rows_flagged` integer, required — Number of rows on the page carrying a trust flag.
    - `row_weighted_unresolved_rate` number, required — Row-weighted mean unresolved_rate across the page.
    - `expected_miss_rate` number, required — Estimated share of true matches absent from arrived data due to ingestion lag.
    - `suppressed_scopes` integer, required — Number of footprint scopes excluded from the result by suppression.

## Other responses

- `422` — Validation Error

---

[API](https://skmtc.net/shovels/apis/the-shovels-api-v2.md) · [All operations](https://skmtc.net/shovels/apis/the-shovels-api-v2/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/shovels/the-shovels-api-v2/versions/d3f0e1f3afa8/schema)
