logicsrc/docs/openproperty.md
Anthony Ettinger 32bbdeda95
OpenListing 0.1: one file a seller serves about a thing on offer (#213)
A house for sale, an apartment to rent, a car. One document per listing,
on the seller's own origin, so a directory reads the seller's file
instead of licensing somebody else's database or scraping a marketplace
that forbids it.

**The name `OpenRental` was the obvious one and it is taken.** It
already means renting OpenAgent and OpenSwarm members through CoinPay,
with a published schema, SDK, validators and fixtures. Its own document
records surviving one collision already: it was OpenFleet until
2026-09-13. Reusing the name for housing would have broken a shipped
0.1 spec, so the family is OpenListing instead.

Two axes rather than a spec per combination. A house for sale and a
house to rent are the same house; only the offer differs. A house for
sale and a car for sale are the same offer; only the subject differs.
Writing OpenHouseForSale, OpenHouseForRent, OpenCarForSale and the rest
produces one spec per cell of a grid, each repeating most of the others.
So `offer.type` carries sale/rent/lease/auction/free/wanted and
`subject.type` carries property/vehicle, and a subject profile adds only
the fields peculiar to its subject. A reader that knows OpenListing but
not a profile still reads price, location, offer and dates correctly.

`openhouse` is not used as a name: in real estate an open house is a
viewing event, not a kind of listing. An actual one is an entry in
`showings`.

The problem section is grounded rather than asserted: RESO tracks 484
separate MLSs as of August 2026, down from nearly twice that in 2015;
each does publish a RESO Web API feed, and each requires a real estate
licence, a signed per-MLS data agreement and vendor credentials. Four
hundred and eighty-four negotiations for one national view, not
redistributable. Vehicle parts are the same shape with different
letters (ACES/VCdb, TecDoc).

Decisions worth naming. Prices are decimal STRINGS, because 2450.10 is
not representable in binary floating point and a listing is a price.
Absent means unstated, never zero -- with `bedrooms: 0` called out in
the property profile as the one place where zero genuinely differs and a
studio is not an unstated bedroom count. `location.precision` lets a
seller publish a postal code rather than a street for an occupied home,
and forbids a reader from drawing a pin on a building anyway. A closed
listing SHOULD stay served for 90 days with `closed_at`, because a
directory that learns from a 404 cannot tell "sold" from "outage" --
the failure that makes every coupon site a graveyard.

The vehicle profile treats a VIN as a claim rather than a proof, points
readers at NHTSA vPIC (free, keyless, public domain) for decoding rather
than making sellers restate what the VIN encodes, and puts parts fitment
explicitly out of scope: that is ACES/VCdb or TecDoc, and a public spec
cannot restate licensed data.

Registered as one parent and two blocks under it, using the registry's
existing `parent` field the way OpenServer's blocks already do. Flat
slugs rather than nested paths because /docs/[slug] is a single dynamic
segment, not a catch-all -- a nested slug would not have routed at all.

nichedb.dev is named as the reference directory.

50 contract tests pass including spec-discovery and page-metadata.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 03:14:19 -07:00

78 lines
4.3 KiB
Markdown

# OpenProperty
The property profile of [OpenListing](/docs/openlisting). It adds one key, `subject.property`, describing a house, an apartment, a room, a plot of land or a commercial unit. Everything else — the offer, the price, the location, the media, the provenance — is the parent specification and is not restated here.
Status: **0.1 draft**, alongside the parent.
Slug: `openproperty`
## When this profile applies
When `subject.type` is `"property"`. A reader that does not know this profile still reads the whole listing correctly; it simply does not know how many bedrooms the thing has.
## `subject.property`
```json
"subject": {
"type": "property",
"title": "Two-bedroom flat on Elm Street",
"property": {
"kind": "apartment",
"bedrooms": 2,
"bathrooms": 1.5,
"floor_area": { "value": 780, "unit": "sqft" },
"lot_area": { "value": 0.12, "unit": "acre" },
"year_built": 1974,
"floors": 1,
"floor": 3,
"parking": { "spaces": 1, "kind": "street" },
"furnished": "unfurnished",
"heating": "gas",
"pets": "cats-only",
"features": ["balcony", "dishwasher"],
"energy": { "rating": "C", "scheme": "EPC" }
}
}
```
### `kind`
`house`, `apartment`, `condo`, `townhouse`, `room`, `land`, `commercial`, `parking`, `storage`, `other`.
`room` is a room within a dwelling let separately, which is a different thing from a one-bedroom apartment and is routinely mislabelled as one. `land` carries `lot_area` and usually no `floor_area`.
### Fields
| Field | Notes |
| --- | --- |
| `bedrooms` | An integer. A studio is `0`, not absent — this is the one place absent and zero genuinely differ and the difference matters. |
| `bathrooms` | A number, because half-baths are real and `1.5` is the ordinary way to write one. |
| `floor_area` | `value` + `unit` (`sqft`, `sqm`). Internal area. |
| `lot_area` | `value` + `unit` (`sqft`, `sqm`, `acre`, `hectare`). |
| `year_built` | Four-digit year. |
| `floors` | How many storeys the dwelling has. |
| `floor` | Which storey it is on. Distinct from `floors`, and confusing them is the commonest error in property data. Ground floor is `0` in the UK sense and `1` in the US sense, so a publisher SHOULD also set `floor_scheme` to `uk` or `us` when it sets `floor`. |
| `parking` | `spaces` and `kind` (`garage`, `driveway`, `street`, `permit`, `none`). |
| `furnished` | `furnished`, `part-furnished`, `unfurnished`. |
| `heating` | Free text; no controlled vocabulary, because national ones disagree. |
| `pets` | `allowed`, `none`, `cats-only`, `dogs-only`, `ask`. |
| `accessibility` | `step_free`, `lift`, `wet_room` and similar, as a list. |
| `features` | Free-text list. Deliberately unconstrained: this is where everything a controlled vocabulary would lose goes. |
| `energy` | `rating` and `scheme` (`EPC`, `HES`, `NABERS`). The scheme is required alongside the rating, because a bare "C" means nothing without it. |
All are optional. Absent means unstated, exactly as in the parent, with `bedrooms: 0` noted above as the meaningful exception.
## Tenure
For a sale, `subject.property.tenure` MAY be `freehold`, `leasehold`, `commonhold`, `share-of-freehold` or `other`, with `lease_years_remaining` where it applies. A leasehold with eighty years left is a materially different asset from the same flat freehold, and a listing that omits this is omitting the thing a buyer most needs.
## What this profile does not do
- **No valuation.** A listing says the asking price. What the thing is worth is somebody else's claim, and [OpenOntology](/docs/openontology) is where a claim with a source belongs.
- **No condition or survey.** "Needs work" is `description` text, not a field, because every attempt to enumerate condition ends up either useless or misleading.
- **No floorplan geometry.** A floorplan is media with `kind: "floorplan"`. Describing rooms as geometry is a different specification and a much larger one.
- **No address normalisation.** The parent's `location` carries what the seller stated, at the precision the seller chose. Normalising it is a consumer's job.
## Market context is not a listing
Aggregate market data — median sale price for a ZIP, days on market, inventory — is **not** an OpenListing document and MUST NOT be published as one. It describes a market, not a thing on offer, and it has no `offer` and no seller. Directories that carry both keep them apart.