--- title: Type System & Advanced | Plaza Docs description: PlazaQL's type inference, chain ordering rules, expression language, structural joins, narrowing, and attribute access. --- ## Type system PlazaQL has a fully inferred type system. You never declare types — the compiler tracks them through your chain and catches errors. ### How types flow ``` search(node, amenity: "cafe") // → PointSet .around(500, point(38.9, -77.0)) // → PointSet (spatial filter preserves type) .centroid() // → PointSet (centroid of points = points) .limit(count: 10); // → PointSet ``` ``` search(amenity: "cafe") // → GeoSet (no type = mixed) .within(geometry: $berlin) // → GeoSet .buffer(50) // → PolygonSet (buffer always → polygons) .simplify(10); // → PolygonSet ``` ### Chain ordering Methods are grouped into three ordering categories: | Group | Methods | Rule | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- | | **Freely orderable** | spatial (`.within()`, `.around()`, `.bbox()`, …), `.filter()`, transforms (`.buffer()`, `.simplify()`, `.centroid()`), joins (`.member_of()`, `.has_member()`), computed columns (`.elevation()`, `.distance()`, …), output shape (`.fields()`, `.precision()`, …) | Can appear in any order relative to each other | | **Narrowing** | `.first()`, `.last()`, `.index()` | Converts `GeoSet` → `GeoElement`; must come after spatial/join filters | | **Ordering** | `.sort()`, `.limit()`, `.offset()` | Must come after all freely orderable methods | | **Output mode** | `.count()`, `.ids()`, `.tags()`, `.skel()`, `.geom()` | Must be last in chain, max one | | **Aggregation** | `.sum()`, `.avg()`, `.min()`, `.max()`, `.group_by().count()` | Terminal — must be last in chain | Spatial filters, tag filters, transforms, computed columns, and output shape methods can be freely mixed and reordered. This means all of these are valid: ``` // Buffer before spatial filter search(amenity: "school") .buffer(50) .within(geometry: $city); // Filter after transform search(node, amenity: "cafe") .buffer(50) .filter(cuisine: "italian"); // Computed column before transform search(node, amenity: "cafe") .within(geometry: $berlin) .elevation() .buffer(50); ``` Additional constraints: - `.limit()` must come before `.offset()` ### Error messages PlazaQL gives precise, actionable errors with source locations: ``` error: .buffer() (transform) cannot follow .limit() (ordering) — ordering methods must come after all other methods --> query:3:5 | 3 | .limit(10).buffer(50) | ^^^^^^^^^^^ transform after ordering | = hint: move .buffer(50) before .limit(10) ``` ``` error: .count() is terminal — no methods can follow it --> query:4:5 | 4 | .count().limit(10) | ^^^^^^^^^ not allowed after terminal | = hint: remove .limit(10) or move it before .count() ``` ### Keyword vs positional arguments All functions support both styles. You cannot mix them in a single call: ``` // Keyword (preferred — self-documenting) .around(distance: 500, geometry: point(lat: 38.9, lng: -77.0)) // Positional (concise) .around(500, point(38.9, -77.0)) ``` ## Expressions PlazaQL has a full expression language for computing values from tags, element properties, and geometry. Expressions are used in `.filter()`, aggregation functions (`.sum()`, `.avg()`), and `.group_by()`. ### Tag access — `t["key"]` Access any tag value with `t["key"]`. Tag values are always strings in OSM: ``` t["name"] // → "Starbucks" t["amenity"] // → "cafe" t["building:levels"] // → "5" t["capacity"] // → "200" ``` ### Property accessors Built-in functions that return element metadata: | Function | Returns | Example | | -------- | ----------------------------- | ----------------- | | `id()` | OSM ID (integer) | `id() > 1000000` | | `type()` | Element type string | `type() == "way"` | | `lat()` | Latitude (float, nodes only) | `lat() > 48.0` | | `lon()` | Longitude (float, nodes only) | `lon() < 2.5` | ### Geometry functions Compute geometric properties of each feature: | Function | Returns | Description | | ------------- | --------------------- | -------------------------------------------------------------------- | | `length()` | meters (float) | Geometric length of a way. Returns 0 for nodes. | | `area()` | square meters (float) | Area of a closed way or relation. Returns 0 for open ways and nodes. | | `is_closed()` | boolean | Whether a way’s first and last nodes are the same. | `length()` is always geometric length in meters. For string length, use `size()`. ### Type coercion — `number()` Tag values are strings. Use `number()` to cast them for arithmetic: ``` number(t["capacity"]) // "200" → 200 number(t["height"]) // "12.5" → 12.5 number(t["lanes"]) // "3" → 3 ``` `number()` is required for arithmetic on tag values. Without it, `t["capacity"] > 50` compares strings, not numbers. `is_number()` tests whether a tag value can be parsed as a number (returns boolean): ``` // Only include elements where population is a valid number .filter(is_number(t["population"])) ``` ### String functions | Function | Description | Example | | --------------------------- | ------------------------------ | --------------------------------------- | | `starts_with(str, prefix)` | True if str starts with prefix | `starts_with(t["name"], "St")` | | `ends_with(str, suffix)` | True if str ends with suffix | `ends_with(t["name"], "Park")` | | `str_contains(str, substr)` | True if str contains substr | `str_contains(t["cuisine"], "italian")` | | `size(str)` | String length (characters) | `size(t["name"]) > 3` | `str_contains()` is used instead of `contains()` to avoid conflict with the spatial `.contains()` method. `size()` is used instead of `length()` to avoid conflict with geometric `length()`. ### Operators Arithmetic (requires `number()` for tag values): | Operator | Description | Example | | -------- | -------------- | ------------------------------------------ | | `+` | Addition | `number(t["lanes"]) + 1` | | `-` | Subtraction | `number(t["height"]) - 3` | | `*` | Multiplication | `number(t["width"]) * number(t["length"])` | | `/` | Division | `area() / 1000` | Comparison: | Operator | Description | Example | | -------- | ---------------- | ---------------------------- | | `>` | Greater than | `number(t["capacity"]) > 50` | | `<` | Less than | `length() < 100` | | `>=` | Greater or equal | `number(t["height"]) >= 10` | | `<=` | Less or equal | `area() <= 500` | | `==` | Equal | `type() == "way"` | | `!=` | Not equal | `t["name"] != "Unknown"` | Logical: | Operator | Description | Example | | -------- | ----------- | ---------------------------------------------------------- | | `&&` | Logical AND | `number(t["lanes"]) > 2 && length() > 500` | | `\|\|` | Logical OR | `t["amenity"] == "cafe" \|\| t["amenity"] == "restaurant"` | | `!` | Logical NOT | `!is_closed()` | ### Operator precedence (highest to lowest) 1. `!` (unary NOT) 2. `*`, `/` 3. `+`, `-` 4. `>`, `<`, `>=`, `<=` 5. `==`, `!=` 6. `&&` 7. `||` Use parentheses to override: `(a || b) && c`. ### Expression examples ``` // Roads longer than 1km with more than 2 lanes search(way, highway: *) .within(geometry: $city) .filter(length() > 1000 && number(t["lanes"]) > 2); // Large buildings (area over 1000 sq meters) search(way, building: *) .bbox(40.7, -74.0, 40.8, -73.9) .filter(area() > 1000); // Closed ways that aren't tagged as buildings search(way) .within(geometry: $area) .filter(is_closed() && t["building"] == !*); // Restaurants with valid capacity over 100 search(amenity: "restaurant") .within(geometry: $city) .filter(is_number(t["capacity"]) && number(t["capacity"]) > 100); // Total lane-kilometers of roads in a city search(way, highway: *) .within(geometry: $city) .filter(is_number(t["lanes"])) .sum(number(t["lanes"]) * length() / 1000); ``` ## Structural joins Structural joins let you navigate the membership graph — relations contain members (nodes, ways, other relations), and ways contain nodes. Two methods express this relationship from opposite directions. ### `.member_of()` — filter to members of a source Keep only elements that are members of the given source set. The first argument is a variable reference or an inline `search()` expression. Use the optional `role:` keyword argument to filter by membership role. ``` // Bus stops that belong to route 42 $$ = search(node, highway: "bus_stop") .member_of(search(relation, route: "bus", ref: "42"), role: "stop"); ``` `.member_of()` works with relations (which have typed members) and ways (which contain ordered nodes). ### `.has_member()` — filter to elements containing a source The reverse of `.member_of()`. Keep only elements that contain the given source elements as members. ``` // All subway lines that pass through a specific station $my_stop = search(node, railway: "station") .around(distance: 500, lat: 40.75, lng: -73.99).first(); $$ = search(relation, route: "subway").has_member($my_stop); ``` ## Narrowing Narrowing methods transform a set into a single element (`GeoSet` → `GeoElement`). This distinction matters for the type system — see [Attribute access](#attribute-access) below. ### `.first()` — first element ``` $stop = search(node, highway: "bus_stop") .around(distance: 500, lat: 40.75, lng: -73.99).first(); ``` ### `.last()` — last element ``` $end = search(node, highway: "bus_stop") .within(geometry: $route).last(); ``` ### `.index(n)` — nth element (1-indexed) ``` $second = search(node, amenity: "cafe") .around(distance: 1000, geometry: point(48.85, 2.35)).index(2); ``` ## Attribute access Extract tag values from variables using bracket syntax: `$var[attr]`. ``` // Get the ref tag from $stop $stop[ref] // Use as a filter value search(node, highway: "bus_stop", ref: $stop[ref]); ``` The type of the variable determines how the attribute value is used in SQL: - **On a `GeoElement`** (after `.first()`, `.last()`, or `.index()`): produces a scalar — exact match with `=` in SQL. - **On a `GeoSet`** (no narrowing): produces a value set — multi-value match with `ANY(...)` in SQL. ### Example queries ``` // Bus stops on route 42 $$ = search(node, highway: "bus_stop") .member_of(search(relation, route: "bus", ref: "42"), role: "stop"); // Subway transfer map — all stations on lines through a nearby stop $my_stop = search(node, railway: "station") .around(distance: 500, lat: 40.75, lng: -73.99).first(); $lines = search(relation, route: "subway").has_member($my_stop); $$ = search(node, railway: "station").member_of($lines); // Attribute join — all stops sharing the same ref as a nearby stop $stop = search(node, highway: "bus_stop") .around(distance: 500, lat: 40.75, lng: -73.99).first(); $$ = search(node, highway: "bus_stop", ref: $stop[ref]) .bbox(40.7, -74.0, 40.8, -73.9); // Cross-source — route ref matched against a custom dataset $route = search(relation, route: "bus").bbox(40.7, -74.0, 40.8, -73.9).first(); $$ = search(point, dataset: "my_transit", route_ref: $route[ref]); ``` ## Resources - **Playground** — Try PlazaQL interactively at [plaza.fyi/playground](https://plaza.fyi/playground) - **API Reference** — Full endpoint documentation in the [API Reference](/api/index.md) - **GitHub** — Language spec, grammars, and examples at [github.com/plazafyi/plazaql](https://github.com/plazafyi/plazaql)