Skip to content

About features

You are viewing in-progress documentation for v2 (Beta). Switch to the stable version for the current production release.

A feature is a named, reusable piece of vector geography attached to a domain: the roads and water bodies that run through your area, or a set of custom fuelbed polygons you supply yourself. Where a domain fixes where you are working and a grid is the rasterized fuelbed, a feature is the vector layer in between — clean lines and polygons describing things that the fuelbed has to account for but that aren’t themselves a raster.

This page is about the idea of a feature: what it stores, why the v2 API has a distinct resource for vector geometry at all, and the mental model that connects a layerset of fuel polygons, a road network pulled from OpenStreetMap, and the two very different things you can do with either. It is not a walk-through of any request — the four feature how-to guides cover the procedures — and it does not re-teach the rule syntax for masking and removal, which lives in About modifications. Here we stay with the concepts those pages assume.

A feature is a named vector dataset, not a single shape

Section titled “A feature is a named vector dataset, not a single shape”

The word “feature” does double duty, and untangling it is the first step to understanding the resource.

  • A Feature (the resource) is one named, persisted dataset: a row of metadata in the database — id, domain_id, type, name, a status, a georeference — that points at a stored blob of vector geometry. A single road Feature can hold thousands of individual road segments.
  • A feature (lowercase, in the GeoJSON sense) is one geometry inside that blob — a single road segment, one lake, one fuelbed polygon.

So a road Feature contains many GeoJSON features. When this page says “feature” it means the resource; when it means one shape it says “geometry” or “polygon.” The API keeps the same separation: the resource is addressed at /v2/domains/{domain_id}/features/{feature_id}, and the geometries inside it are streamed separately (see How the geometry is stored).

Every Feature is exactly one of three types, fixed at creation and never changed:

TypeHoldsSourced from
roadthe road network as footprint polygonsOpenStreetMap (async)
waterlakes, rivers, and streams as areal polygonsOpenStreetMap (async)
layersetcustom fuelbed polygons carrying fuel propertiesa GeoJSON you upload (sync)

Roads and water are geography you fetch; a layerset is fuel you author. That split — fetched reference geometry versus authored fuel input — is the through-line of everything below.

A feature could, in principle, be inlined: you could paste a polygon straight into a grid’s masking rule and never persist anything. The v2 API makes vector geometry a first-class resource anyway, for two reasons.

Reusability. Fetching roads and water from OpenStreetMap is slow — the server has to read a regional extract, clip it to your domain, buffer linework, and reproject it. You don’t want to pay that on every grid and every inventory. Minting the road network once as a Feature turns it into a single source of truth that any number of grids and inventories can reference by id. Edit the road network in one place and every rule that points at it sees the change; a road that is the same road across ten resources is stored once, not ten times.

The vector↔raster bridge. This is the deeper reason. Fuel models run on rasters (grids of cells) and point sets (inventories of trees), but the real-world things you need to reason about — a highway, a shoreline, a managed plot — are naturally vector: precise lines and polygons with sharp edges. A road carries no surface fuel and shouldn’t have trees standing in it, but a grid only knows cells and an inventory only knows points. A feature is the clean vector description of that road, held in a form that the raster and point-set consumers can each be reconciled against. It lets you say “the road,” exactly, once, and then apply it to data models that have no native notion of a road at all.

Domain ── the spatial frame: extent + CRS
├─ Features ── vector geography (roads, water, layerset polygons)
│ │
│ ├─ mask ──▶ Grids (raster: zero/scale fuel in cells)
│ └─ remove ─▶ Inventories (points: drop trees inside the shape)
└─ a layerset can also be rasterized directly into a Grid

Layersets: vector geography as custom fuel

Section titled “Layersets: vector geography as custom fuel”

A layerset is the specialized feature type for custom fuel inputs, and it is worth understanding as its own thing because it inverts the usual direction. Roads and water are reference geometry you mask out of a fuelbed; a layerset is fuelbed — a way to author surface fuel by hand and bring it in.

Concretely, a layerset is a GeoJSON FeatureCollection in which every polygon is a fuelbed, and the polygon’s properties carry that fuelbed’s inputs:

PropertyMeaning
fuel_typea free-form category label ("shrub", "herb", "litter")
fuel_loadingfuel mass per unit ground area
fuel_heightfuel height above the surface
percent_coverfractional cover, in percent (0–100)
distributionhow cover is laid down: homogeneous, uniform_random, or random_clusters
patch_sizeclump diameter — required when distribution is random_clusters
live_fuel_moisture, dead_fuel_moisture, heat_of_combustion, patch_std_devoptional refinements

These names are not arbitrary: they map one-to-one onto fastfuels_core.rasterize_layerset, the function that turns the polygons into a gridded surface-fuel array. A layerset is, in effect, the rasterizer’s input table expressed as geography — each polygon a row, each property a column. That’s why the upload contract is strict about them (every fuelbed must carry the required columns, percent_cover must be in [0, 100], a random_clusters fuelbed must declare its patch_size): the API is validating that the polygons will actually rasterize.

Because a layerset already is fuel, it has a path the other types don’t: it can be rasterized directly into a grid rather than only masking one. The SDK exposes this as feature.rasterize(...), which is valid only for layerset features and returns a new fuel grid. So a layerset participates in fuel modeling in two modes — as a polygon you can mask or remove against like any other feature, and as a fuelbed you can turn into a grid outright.

Two ways to source a feature: OSM and upload

Section titled “Two ways to source a feature: OSM and upload”

How a feature comes into being depends on its type, and the two paths trade off flexibility against immediacy.

OpenStreetMap (road and water) is asynchronous. Creating a road or water feature kicks off a background job. The create call returns immediately with status: "pending"; the job then walks pending → running → completed (or failed). While it runs, the server reads the regional OSM data for your domain’s footprint, builds the geometry, reprojects it into the domain’s CRS, and clips it to the domain’s extent. Only when it lands on completed is the feature’s georeference (its CRS and bounding box) populated and its geometry readable. You poll until then. The trade-off is flexibility for time: you get a real road network for any CONUS domain with a single call, but you wait for the fetch and you take whatever OSM has (see OpenStreetMap is incomplete).

Uploads (layersets) are synchronous. Posting a layerset validates and stores it in the same request: the call returns 201 already completed, with its georeference set. There is no job and no polling — but the burden of correctness is on you. The GeoJSON must already be in a projected CRS (a layerset in a geographic CRS like EPSG:4326 is rejected, because rasterization needs cell sizes in meters), the geometries must be polygons, and every fuelbed must carry valid properties. Immediacy in exchange for doing the preparation yourself.

There is one subtlety worth calling out about OSM roads. OpenStreetMap stores roads as centerlines — one-dimensional lines with no width. A line has no area, and “the fuel under the road” or “the trees on the road” are questions about an area. So the extractor buffers each centerline into a footprint polygon, widening it by road class: a footpath gets a meter or so, a residential street a few meters, a motorway tens of meters. The result is a polygon that approximates the road’s real on-the-ground footprint, which is what every downstream test actually needs. (Water is treated similarly — lakes and wide rivers arrive as polygons already, while narrow streams and canals are buffered from their centerlines.)

This is why a completed road feature holds polygons, not lines, and why an intersects test against a grid catches a road’s full width with no extra work. It is also the setup for the most common point of confusion, addressed in the two uses below: a road footprint is real but thin, and a thin polygon interacts very differently with cells than with points.

A feature’s geometry doesn’t live in the feature document — that holds only metadata. The geometry is written to a separate GeoParquet blob, partitioned into row groups so a feature with thousands of geometries can be read in pieces rather than all at once. Two endpoints expose it:

  • GET .../data/metadata returns the shape of the dataset: total_features (how many geometries the feature holds), partition_count, and the per- partition row counts. It reads only the Parquet footer, so it’s cheap.
  • GET .../data/{partition_index} returns one partition as GeoJSON (application/geo+json) in the domain’s CRS. Iterate the index from 0 to partition_count to stream the whole dataset.

The split matters conceptually: total_features is how you find out whether a feature is empty before you rely on it — the verification step described below — without downloading any geometry.

The two uses: mask a grid, remove from an inventory

Section titled “The two uses: mask a grid, remove from an inventory”

Once a feature exists, there are two things you do with it, and they are genuinely different actions on genuinely different data models. Confusing them is the most common conceptual mistake, so it’s worth being precise.

Masking a grid changes the value of cells the feature touches. A grid is a raster — a fixed lattice where a cell always exists at its position. You cannot delete a cell; the lattice has to stay rectangular and complete. So masking scales or zeroes a fuel band in the affected cells: replace the load with 0 where a road runs (no fuel under the highway), or multiply it by 0.1 to thin fuel along a vegetated shoulder. The cells remain; their fuel drops. There is deliberately no “remove” for grids.

Removing from an inventory deletes trees the feature contains. An inventory is a point set — a list of individual trees, each at an (x, y). A point can simply be dropped from the list. So a remove action against a water feature deletes every tree whose point falls inside the (buffered) water polygon: there shouldn’t be trees standing in a lake, so they’re gone, not scaled.

Same feature, two data models, two verbs:

You have a…which is a…so the action is…because…
Gridraster of cellsmask (scale/zero a band)a cell can’t be deleted — only revalued
Inventoryset of pointsremove (drop trees)a point can be deleted outright

This also explains a sharp asymmetry in buffering. A road footprint is a thin polygon. Test it against a grid with cell-targeting and it catches every cell it overlaps — thinness is no obstacle, because cells have area. Test the same thin polygon against tree points and a bare within catches almost nothing: a point has near-zero probability of landing inside a hairline-thin footprint. That’s why removing trees near a feature nearly always needs a buffer_m to give the geometry width, while masking a grid often doesn’t. The shape didn’t change; the consumer’s geometry did.

The full vocabulary for expressing these actions — the within/intersects/ outside operators, cell-versus-centroid targeting for grids, buffer_m semantics, ANDing conditions within a rule and ORing across rules, and when to reference a Feature by id versus inlining a one-off geometry — is the subject of About modifications. That page is the reference for how to write the rule; this section is only about why masking and removal are different acts.

OpenStreetMap is incomplete: verify before you trust

Section titled “OpenStreetMap is incomplete: verify before you trust”

OpenStreetMap is volunteer-contributed, and its coverage is uneven. Urban and suburban areas are mapped densely; wilderness, remote terrain, and small water bodies are often sparse or absent entirely. A domain deep in a roadless area can return a road feature with zero geometries — a perfectly valid completed feature that simply contains nothing. Tagging conventions also vary by region, so the same physical feature can be classified differently from place to place.

This has a quiet failure mode. An empty feature doesn’t error when you mask or remove with it — it just does nothing: a mask against zero geometry leaves every cell untouched, and you get a “successful” grid that masked nothing. (An inventory removal against an empty feature is less forgiving — it can fail during processing rather than silently no-op.) Either way, the fix is the same: check total_features from data/metadata before you rely on a feature. If it’s zero, OSM has nothing for your extent. Widening the catchment with extent_buffer_m can help when the feature you want sits just outside the domain; beyond that, the data isn’t there and a hand-authored layerset or uploaded geometry is the alternative.

The general principle: a feature’s existence and its completed status tell you the fetch succeeded, not that it found anything. The count is the truth.

A feature belongs to exactly one domain and inherits that domain’s CRS and spatial frame. That ownership shapes everything about its lifecycle.

Mutable versus immutable. A feature has the same sharp split a domain does. Its descriptive metadata — name, description, tags — is freely editable; updating it advances modified_on and changes nothing about the geometry. The geometry itself is immutable. Once a feature is built, you cannot re-fetch or re-shape it in place; to change the geometry you delete the feature and create a new one. This is deliberate, and for the same reason a domain’s frame is frozen: grids and inventories reference features by id, and a rule that masks “the roads” should keep meaning the same roads on every rebuild. If geometry could shift underneath a reference, every downstream resource that points at it would silently change the next time it was processed.

Referenced, not owned. Grids and inventories don’t contain a feature; they hold a feature_id that points at it, resolved at processing time. The binding is validated to be same-domain: a grid or inventory can only reference a feature in its own domain, and a cross-domain reference is rejected. This is the same scoping rule that governs everything in v2 — a domain is the unit of geographic scope, and “the same place, in the same CRS” is only guaranteed within one domain. A feature reaching across domains would break that guarantee.

Because the reference is resolved lazily at build time, treat a feature that rules depend on as a stable artifact. Deleting or recreating it changes what every referencing rule sees on its next rebuild — which is exactly the reproducibility contract the modifications page describes from the other side.

The procedures that put all of this to work are the four feature how-to guides:

For the rule syntax those last two assume — conditions, actions, operators, buffer_m, and feature-versus-inline geometry — see About modifications.

The sibling resource-explanation pages put a feature in context: About domains is the spatial frame a feature attaches to; Grids are what masking acts on; Inventories are what removal acts on. A feature is the vector layer that connects the clean geometry of the real world to both.