Market Boundaries
Overview
Section titled “Overview”A market boundary is a polygon that defines a market area — Greater London Office, Manchester Industrial, and so on. Boundaries are hierarchical (one boundary can have a parent boundary) and they’re what every scheme and address gets auto-linked to when their location falls inside the polygon.
Editing a boundary is admin-gated and uses the in-app polygon editor. The geometry rules below describe what the editor (and the server) will accept; the rest of the page describes the lifecycle around a save — including the address-and-scheme re-sync that re-evaluates every address against the new shape.
Save is only enabled once you’ve changed something.
Required fields
Section titled “Required fields”| Field | Required when | What you see if missing |
|---|---|---|
| Code | Always | ”Code is required.” |
| Name | Always | ”Name is required.” |
| Polygon | Always (on save through the editor) | “Polygon is required.” |
The Market Type and Market are required at the model level but the form supplies a default for both, so you only hit a missing-required error if you actively clear them.
Field-level rules
Section titled “Field-level rules”Code max 50 characters
Section titled “Code max 50 characters”The form rejects codes longer than 50 characters with “Code too long.”
Name max 255 characters
Section titled “Name max 255 characters”The form rejects names longer than 255 characters with “Name too long.”
Description max 500 characters
Section titled “Description max 500 characters”The optional Description field is capped at 500 characters.
Where this lives: src/services/web/src/lib/validation/market-boundary.ts.
Geometry rules
Section titled “Geometry rules”The polygon you draw or paste in is checked against six rules. Some fail the save outright; others surface as warnings while you edit but don’t block.
Must be a single polygon
Section titled “Must be a single polygon”MultiPolygons (a shape made of two or more disconnected pieces) are rejected: “Expected a single Polygon geometry but got MultiPolygon.” If you load an existing boundary that’s stored as a MultiPolygon (some old data is), the editor automatically collapses it to the largest ring on load so you can edit and re-save as a single polygon.
At least 4 points
Section titled “At least 4 points”A polygon needs at least 4 coordinates to form a closed ring (the first and last point are the same point, so 4 distinct points around the perimeter is the minimum). Anything less: “Polygon has only N point(s); a closed ring requires at least 4.”
Non-zero area
Section titled “Non-zero area”A degenerate polygon (one whose points are collinear, producing zero area) is rejected: “Polygon has zero area.”
Coordinates must be valid WGS84
Section titled “Coordinates must be valid WGS84”Every coordinate must be within the valid WGS84 range — longitude between −180 and 180, latitude between −90 and 90. The first out-of-range coordinate encountered is reported: “Coordinate (X, Y) is outside the WGS84 valid range.”
Must be topologically valid
Section titled “Must be topologically valid”The polygon is run through a topology check that catches self-intersections (the boundary crosses itself), unclosed rings, duplicate consecutive points, and similar geometric problems. The check reports the first issue it finds along with the coordinate where it occurs.
Vertex caps
Section titled “Vertex caps”Polygon vertex counts are bounded in three tiers:
| Vertex count | What happens |
|---|---|
| Up to 2,500 | No warning. |
| 2,500 — 5,000 | The editor surfaces an informational message (the boundary is on the larger side but still acceptable). |
| 5,000 — 10,000 | The editor shows a stronger warning (very large; consider simplifying). |
| Over 10,000 | The save is rejected: “Polygon has N vertices; the maximum allowed is 10,000. Simplify before saving.” |
The editor’s Save button is disabled when the polygon is over the ceiling so you can’t even submit it. Simplify the polygon (drop intermediate points along straight edges) and try again.
Where this lives: src/common/services/Validation/GeometryValidationService.cs (validity checks) and src/common/services/Validation/IGeometryValidationService.cs (vertex thresholds).
Coordinates rounded to 6 decimal places on load
Section titled “Coordinates rounded to 6 decimal places on load”When the editor loads an existing polygon, the coordinates are rounded to 6 decimal places (about 10cm precision on the ground). SQL Server’s geography round-trip produces 15+ digits of precision, which the polygon editor library rejects as “excessive precision” — so the round happens on load. The rounded shape is what you edit and save.
On save
Section titled “On save”Polygon validation
Section titled “Polygon validation”The server re-runs all six geometry rules against the polygon you submit, regardless of whether the editor passed them client-side. A boundary that fails any of them is rejected with the matching error.
Cache version bump
Section titled “Cache version bump”Once the boundary is saved, the MarketBoundary cache version bumps. The next time a list-page map asks for boundary tiles, the new version is in the URL, so any browser / CDN cache that was holding the previous version of the boundary drops it and fetches fresh. The change appears on every user’s map within the cache-refresh window.
Concurrency check
Section titled “Concurrency check”The save carries the RowVersion of the boundary you loaded. If another admin has updated it since, you get a concurrency error. See Concurrent edits.
After save
Section titled “After save”Address and scheme re-sync
Section titled “Address and scheme re-sync”The shape change triggers a re-evaluation of every address (and through them, every scheme) against the union of the old polygon and the new polygon. Addresses that have moved into the boundary get a new AddressMarketBoundary link; addresses that have moved out lose theirs. This runs as a domain event handler so it happens reliably after the save completes, even if the save itself is fast.
The same handler runs on Create (no previous polygon to compare; just sync everything inside the new one) and on Delete (no new polygon; remove every link to the deleted boundary, restoring those addresses’ boundary lists to the remaining boundaries that cover them).
Where this lives: src/services/api/app/app.api/Handlers/Events/MarketBoundaries/MarketBoundaryGeometryChangedEventHandler.cs + AddressMarketBoundaryService.cs.
Tile-rendering exclusions
Section titled “Tile-rendering exclusions”The tile-rendering endpoint (used to draw boundaries on every list-page map) intentionally excludes boundaries whose Parent is null — those are top-level / root rows that are placeholders or aggregates, not features to render. So the very top of the hierarchy doesn’t show up on maps; everything below it does.
If you create a new boundary and it doesn’t appear on the list-page map, check whether it has a parent boundary set.
Audit log
Section titled “Audit log”An audit log entry records who saved what, including the new vertex count and centroid.
Permissions
Section titled “Permissions”Boundary edits are admin-only — only users with the Admin role can create, edit, or delete market boundaries. See Permissions and roles.
Related rules
Section titled “Related rules”- Addresses — the AddressMarketBoundary link is set by spatial intersection. Re-runs whenever an address’s location changes or a boundary’s polygon changes.
- Schemes — schemes inherit their market boundaries from their address.
- Permissions and roles — Admin gating.
- Search matching — boundary code/name search.
- Concurrent edits — RowVersion mechanics.
Where this lives
Section titled “Where this lives”- Frontend validation:
src/services/web/src/lib/validation/market-boundary.ts - Editor:
src/services/web/src/lib/components/maps/MarketBoundaryMapEditor.svelte - Editor composable:
src/services/web/src/lib/composables/use-market-boundary-editor.svelte.ts - Vertex thresholds:
src/services/web/src/lib/market-boundary-limits.ts(frontend mirror) +src/common/services/Validation/IGeometryValidationService.cs(canonical) - Geometry validation service:
src/common/services/Validation/GeometryValidationService.cs - Model:
src/common/models/Models/MarketBoundary.cs - Command handlers:
src/services/api/app/app.api/Handlers/Commands/MarketBoundaries/ - Tile controller:
src/services/api/app/app.api/Controllers/MarketBoundariesVectorController.cs - Address re-sync:
src/services/api/app/app.api/Handlers/Events/MarketBoundaries/MarketBoundaryGeometryChangedEventHandler.cs - Address-boundary spatial sync:
src/services/api/app/app.api/Services/Model/AddressMarketBoundaryService.cs