---
title: "Structured parcel search with a fast LIMIT fetch and a capped count"
description: "Address lookup: send ONE address per call as {field: 'address', operator: 'like', value: '<street> <number>'} with maxResults 10 - values are trimmed, and street + house number matches the stored 'Bahnhofstrasse 1' form. Walk address lists in small sequential batches, not one wide parallel burst. Backs the `choose` app's parcel search (and the navbar EGRID search). Replaces the GeoServer WFS path, whose exact total count over broad filters took 7–30 s and blew the Vercel Edge 25 s wall. This endpoint splits the work: a `LIMIT maxResults` row fetch with NO `ORDER BY` (the client sorts; a stable order would force a full sort and defeat the early exit) and a separate count that stops at 10,001 rows, reported as `numberMatched: 10000, capped: true` (the count is skipped when the fetch returns fewer rows than maxResults - the total is then exact). `like` on `address`/`streetname` is a trigram-indexed contains match (fast); `like` on any other text column still scans.\n\nReads the configured parcel snapshot table (`config.parcelTableName`). Both statements run in one transaction under `SET LOCAL statement_timeout = 20000`; a filter that cannot use an index (notably `like` on `fso_name_2021`) trips it and answers 500 `{\"error\":\"parcel search failed\"}`.\n\nFiltering rules live in `ParcelSearchCondition`: unknown fields and empty values are silently dropped rather than rejected, so a malformed condition list yields a broader search, never a 400. There is no request validation error path at all - the only non-200 answer is the 500.\n\n**No authentication** (mirrors the WFS it replaced; the whole surface is public parcel data). Not cached."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.aireon.ch/llms.txt
> Use this file to discover all available pages before exploring further.

Path: RES API (public) › Parcel

`POST /res_api/parcel_search`

Address lookup: send ONE address per call as {field: 'address', operator: 'like', value: '<street> <number>'} with maxResults 10 - values are trimmed, and street + house number matches the stored 'Bahnhofstrasse 1' form. Walk address lists in small sequential batches, not one wide parallel burst. Backs the `choose` app's parcel search (and the navbar EGRID search). Replaces the GeoServer WFS path, whose exact total count over broad filters took 7–30 s and blew the Vercel Edge 25 s wall. This endpoint splits the work: a `LIMIT maxResults` row fetch with NO `ORDER BY` (the client sorts; a stable order would force a full sort and defeat the early exit) and a separate count that stops at 10,001 rows, reported as `numberMatched: 10000, capped: true` (the count is skipped when the fetch returns fewer rows than maxResults - the total is then exact). `like` on `address`/`streetname` is a trigram-indexed contains match (fast); `like` on any other text column still scans.

Reads the configured parcel snapshot table (`config.parcelTableName`). Both statements run in one transaction under `SET LOCAL statement_timeout = 20000`; a filter that cannot use an index (notably `like` on `fso_name_2021`) trips it and answers 500 `{"error":"parcel search failed"}`.

Filtering rules live in `ParcelSearchCondition`: unknown fields and empty values are silently dropped rather than rejected, so a malformed condition list yields a broader search, never a 400. There is no request validation error path at all - the only non-200 answer is the 500.

**No authentication** (mirrors the WFS it replaced; the whole surface is public parcel data). Not cached.

## Request body

_Required._

- `conditions` (`searchParcels.conditions`; array<object>, optional): Filter clauses, ANDed together. Omit, or send an empty array, to match every parcel (the count is then simply capped).
  - `field` (`searchParcels.conditions.field`; string, required)
    - one of `"parcel_id"`, `"cz_canton_name"`, `"cityname"`, `"districtname"`, `"zip"`, `"streetname"`, `"address"`, `"parcel_area"`, `"is_sell"`, `"ratio_s"`, `"ratio_s_free"`, `"ratio_v"`, `"ratio_v_free"`, `"estimated_price_m2"`, `"bldg_constr_year"`, `"bldg_size"`, `"bldg_vol_gwr"`, `"bldg_vol_sb3dgdb"`, `"bldg_height_mean"`, `"bldg_height_max"`, `"bldg_floors"`, `"bldg_flats"`, `"bldg_rooms"`, `"bldg_count"`, `"gfa_now"`, `"gfa_max"`, `"vol_max"`, `"cz_harmonized"`, `"cz_local"`, `"cz_util_now"`, `"tax_sum"`, `"tax_sum_chf"`, `"vac_all"`, `"vac_new"`, `"tt_agglo_pubt"`, `"tt_agglo_road"`, `"acc_ewap_pubt"`, `"acc_ewap_road"`, `"fso_name_2021"`, `"fso_num_2021"`; example `"cz_canton_name"`
  - `operator` (`searchParcels.conditions.operator`; string, required)
    - one of `"eq"`, `"neq"`, `"gt"`, `"gte"`, `"lt"`, `"lte"`, `"between"`, `"in"`, `"like"`; example `"eq"`
  - `value` (`searchParcels.conditions.value`; one of, optional): Comparison value. Numbers may be sent as numbers or numeric strings; `in` takes ONE comma-separated string (`"8000,8001"`), not an array.
    - example `"Wohnzone"`
    - one of: `string`, `number`, `boolean`
  - `valueTo` (`searchParcels.conditions.valueTo`; one of, optional): Upper bound for `between` on numeric fields. Ignored for every other operator.
    - example `2000`
    - one of: `string`, `number`
- `maxResults` (`searchParcels.maxResults`; integer, optional): Row ceiling. A missing or non-numeric value, or anything below 1, becomes 50; anything above 1000 is clamped to 1000.
  - min 1; max 1000; default `50`

## Example requests

### Residential parcels in Zürich above 1,000 m²

```json
{
  "conditions": [
    {
      "field": "fso_name_2021",
      "operator": "eq",
      "value": "Zürich"
    },
    {
      "field": "cz_harmonized",
      "operator": "like",
      "value": "Wohn"
    },
    {
      "field": "parcel_area",
      "operator": "between",
      "value": 1000,
      "valueTo": 5000
    }
  ],
  "maxResults": 100
}
```

### Navbar EGRID lookup (exact match short-circuit)

```json
{
  "conditions": [
    {
      "field": "parcel_id",
      "operator": "like",
      "value": "CH513579227784"
    }
  ],
  "maxResults": 1
}
```

## Code samples

### cURL

```bash
curl --request POST \
  --url https://res.aireon.ch/res_api/parcel_search \
  --header 'Content-Type: application/json' \
  --data '
{
  "conditions": [
    {
      "field": "fso_name_2021",
      "operator": "eq",
      "value": "Zürich"
    },
    {
      "field": "cz_harmonized",
      "operator": "like",
      "value": "Wohn"
    },
    {
      "field": "parcel_area",
      "operator": "between",
      "value": 1000,
      "valueTo": 5000
    }
  ],
  "maxResults": 100
}
'
```

### TypeScript

```typescript
const url = 'https://res.aireon.ch/res_api/parcel_search';
const options = {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({
    conditions: [
      {field: 'fso_name_2021', operator: 'eq', value: 'Zürich'},
      {field: 'cz_harmonized', operator: 'like', value: 'Wohn'},
      {field: 'parcel_area', operator: 'between', value: 1000, valueTo: 5000}
    ],
    maxResults: 100
  })
};

fetch(url, options)
  .then(res => res.json())
  .then(json => console.log(json))
  .catch(err => console.error(err));
```

### Python

```python
import requests

url = "https://res.aireon.ch/res_api/parcel_search"

payload = {
    "conditions": [
        {
            "field": "fso_name_2021",
            "operator": "eq",
            "value": "Zürich"
        },
        {
            "field": "cz_harmonized",
            "operator": "like",
            "value": "Wohn"
        },
        {
            "field": "parcel_area",
            "operator": "between",
            "value": 1000,
            "valueTo": 5000
        }
    ],
    "maxResults": 100
}
headers = {"Content-Type": "application/json"}

response = requests.post(url, json=payload, headers=headers)

print(response.text)
```

## Responses

### 200

GeoJSON-shaped FeatureCollection with `geometry: null` on every feature, plus the capped count.

#### Example

```json
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "id": "parcel_2025_07.CH513579227784",
      "geometry": null,
      "properties": {
        "parcel_id": "CH513579227784",
        "address": "Bahnhofstrasse 1",
        "cityname": "Kreuzlingen",
        "zip": "8280",
        "parcel_area": 1234.5,
        "is_sell": false,
        "lat": 47.6223657,
        "lng": 8.7478558,
        "cz_local": "Wohnzone 3",
        "fso_name_2021": "Kreuzlingen",
        "fso_num_2021": 4671
      }
    }
  ],
  "totalFeatures": 1,
  "numberMatched": 1,
  "numberReturned": 1,
  "capped": false
}
```

- `type` (`searchParcels.response.200.type`; string, required)
- `features` (`searchParcels.response.200.features`; array<object>, required)
  - `type` (`searchParcels.response.200.features.type`; string, optional)
  - `id` (`searchParcels.response.200.features.id`; string, optional): `<parcel table>.<parcel_id>`, mirroring the WFS feature id this endpoint replaced.
    - example `"parcel_2025_07.CH513579227784"`
  - `geometry` (`searchParcels.response.200.features.geometry`; null, optional): Always null - geometry is intentionally not selected.
  - `properties` (`searchParcels.response.200.features.properties`; object, optional): The whitelisted result columns (`RESULT_COLS`), in this exact key order - the `choose` app builds its CSV header from `Object.keys(features[0].properties)`, so the list is append-only. Values come straight from the parcel snapshot table: any column can be null, and int8/numeric columns arrive as strings.
    - `parcel_id` (`searchParcels.response.200.features.properties.parcel_id`; string | null, optional): EGRID (`CH` + 12 digits).
    - `parcel_local_id` (`searchParcels.response.200.features.properties.parcel_local_id`; string | null, optional)
    - `address` (`searchParcels.response.200.features.properties.address`; string | null, optional)
    - `streetname` (`searchParcels.response.200.features.properties.streetname`; string | null, optional)
    - `cityname` (`searchParcels.response.200.features.properties.cityname`; string | null, optional)
    - `districtname` (`searchParcels.response.200.features.properties.districtname`; string | null, optional)
    - `cz_canton` (`searchParcels.response.200.features.properties.cz_canton`; string | null, optional)
    - `cz_canton_name` (`searchParcels.response.200.features.properties.cz_canton_name`; string | null, optional)
    - `zip` (`searchParcels.response.200.features.properties.zip`; string | integer | null, optional): Postal code. The column is int8, which pg-promise serialises as a string.
    - `parcel_area` (`searchParcels.response.200.features.properties.parcel_area`; number | null, optional)
    - `plot_type` (`searchParcels.response.200.features.properties.plot_type`; string | null, optional)
    - `owner` (`searchParcels.response.200.features.properties.owner`; string | null, optional)
    - `is_sell` (`searchParcels.response.200.features.properties.is_sell`; boolean | null, optional)
    - `bldg_constr_year` (`searchParcels.response.200.features.properties.bldg_constr_year`; number | null, optional)
    - `bldg_size` (`searchParcels.response.200.features.properties.bldg_size`; number | null, optional)
    - `bldg_vol_gwr` (`searchParcels.response.200.features.properties.bldg_vol_gwr`; number | null, optional)
    - `bldg_height_mean` (`searchParcels.response.200.features.properties.bldg_height_mean`; number | null, optional)
    - `bldg_floors` (`searchParcels.response.200.features.properties.bldg_floors`; number | null, optional)
    - `bldg_flats` (`searchParcels.response.200.features.properties.bldg_flats`; number | null, optional)
    - `bldg_count` (`searchParcels.response.200.features.properties.bldg_count`; number | null, optional)
    - `gfa_now` (`searchParcels.response.200.features.properties.gfa_now`; number | null, optional)
    - `gfa_max` (`searchParcels.response.200.features.properties.gfa_max`; number | null, optional)
    - `vol_max` (`searchParcels.response.200.features.properties.vol_max`; number | null, optional)
    - `estimated_price_m2` (`searchParcels.response.200.features.properties.estimated_price_m2`; number | null, optional)
    - `ratio_v_free` (`searchParcels.response.200.features.properties.ratio_v_free`; number | null, optional)
    - `ratio_s_free` (`searchParcels.response.200.features.properties.ratio_s_free`; number | null, optional)
    - `tax_sum_chf` (`searchParcels.response.200.features.properties.tax_sum_chf`; number | null, optional)
    - `lat` (`searchParcels.response.200.features.properties.lat`; number | null, optional): WGS84 latitude of the parcel point (choose renders markers from lat/lng; geometry is never returned).
    - `lng` (`searchParcels.response.200.features.properties.lng`; number | null, optional): WGS84 longitude.
    - `cz_harmonized` (`searchParcels.response.200.features.properties.cz_harmonized`; string | null, optional)
    - `cz_local` (`searchParcels.response.200.features.properties.cz_local`; string | null, optional)
    - `cz_util_now` (`searchParcels.response.200.features.properties.cz_util_now`; number | null, optional)
    - `fso_name_2021` (`searchParcels.response.200.features.properties.fso_name_2021`; string | null, optional): FSO municipality name - never null, unlike `cityname` (address/GWR-derived, null for ~48 % of parcels: every one without a building).
    - `fso_num_2021` (`searchParcels.response.200.features.properties.fso_num_2021`; number | null, optional): BFS/FSO municipality number.
- `totalFeatures` (`searchParcels.response.200.totalFeatures`; integer, required): Same value as `numberMatched` (kept for WFS-shaped clients).
- `numberMatched` (`searchParcels.response.200.numberMatched`; integer, required): Matching rows, counted with a cap: the count query stops at 10,001 rows, so this is at most 10000 and `capped` says whether the true total is larger.
  - example `10000`
- `numberReturned` (`searchParcels.response.200.numberReturned`; integer, required): Rows in `features` (≤ `maxResults`).
  - example `50`
- `capped` (`searchParcels.response.200.capped`; boolean, required): True when more than 10,000 rows match; render as "10,000+".

### 500

Query failed or exceeded the 20 s statement timeout - `{"error":"parcel search failed"}`.

#### Example

```json
{
  "error": "string",
  "message": "string"
}
```

- `error` (`searchParcels.response.500.error`; string, required): Human-readable error description.
- `message` (`searchParcels.response.500.message`; string, optional): Optional underlying error message.


Source: https://docs.aireon.ch/res/search-parcels/index.md
