Set Operations & Outputs
Combine result sets, produce multiple outputs, and control sorting, pagination, output modes, and aggregation in PlazaQL.
Set operations
Section titled “Set operations”Union (+) — combine results
Section titled “Union (+) — combine results”// All food and drink places$food = search(amenity: "restaurant") + search(amenity: "cafe") + search(amenity: "bar");
$food.within(geometry: $city).limit(count: 100);Union preserves types when possible: PointSet + PointSet stays PointSet. Mixed types become GeoSet.
Difference (-) — exclude results
Section titled “Difference (-) — exclude results”// All cafes except Starbuckssearch(amenity: "cafe") - search(name: "Starbucks");
// All food places except fast food$all_food = search(amenity: "restaurant") + search(amenity: "cafe");$fast = search(cuisine: "burger") + search(cuisine: "pizza");($all_food - $fast).within(geometry: $downtown);Difference preserves the left operand’s type.
Intersection (&) — keep only shared results
Section titled “Intersection (&) — keep only shared results”// Cafes that are also Italian$cafes = search(amenity: "cafe");$italian = search(cuisine: "italian");$cafes & $italian;
// Wheelchair-accessible restaurants with outdoor seating$accessible = search(amenity: "restaurant", wheelchair: "yes");$outdoor = search(amenity: "restaurant", outdoor_seating: "yes");($accessible & $outdoor).within(geometry: $city);Intersection keeps only elements present in both sets. Useful for combining independent filter criteria that can’t be expressed in a single search().
Multiple outputs
Section titled “Multiple outputs”A single query can produce multiple named result sets using $$.name:
$nyc = boundary(name: "New York City");
$$.museums = search(tourism: "museum").within(geometry: $nyc);$$.parks = search(leisure: "park").within(geometry: $nyc);$$.transit = search(node, railway: "station").within(geometry: $nyc);Each named output appears as a separate key in the response. This is more efficient than running three separate queries because the area resolution happens once.
Referencing named outputs
Section titled “Referencing named outputs”Named outputs ($$.name) can be referenced as values in later statements — assign once, then use the same name downstream:
$$.route = route(origin: point(40.71, -74.00), destination: point(40.75, -73.98), mode: "foot");$$.stops = search(node, amenity: "cafe").around(distance: 200, geometry: $$.route);$$.route appears in the response AND serves as the geometry for the .around() filter — no intermediate variable needed.
Rules:
- Named outputs are immutable — you cannot reassign
$$.nameonce set - No forward references — define
$$.namebefore referencing it - Use regular
$varvariables for intermediate values that don’t need to appear in the response
For single-output queries, a bare expression works the same as assigning to $$:
// These are equivalentsearch(amenity: "cafe").limit(count: 10);$$ = search(amenity: "cafe").limit(count: 10);Global directives
Section titled “Global directives”When a query has multiple outputs, you often want to apply the same spatial filter or limit to all of them. Global directives let you do this with the # prefix — any chainable method can be used as a directive:
#within(boundary("Berlin"))#limit(100)
$$.cafes = search(amenity: "cafe");$$.bars = search(amenity: "bar");$$.restaurants = search(amenity: "restaurant");This is equivalent to calling .within(geometry: boundary(name: "Berlin")).limit(count: 100) on each output individually. Directives go at the top of the query and apply to every output statement.
Any chainable method works
Section titled “Any chainable method works”The # prefix isn’t a separate set of functions — it’s any method you’d normally chain, written as a directive:
#within(boundary("Berlin"))#bbox(47.0, 10.0, 48.0, 11.0)#filter(amenity: *)#limit(100)#precision(5)Stacking rules
Section titled “Stacking rules”Directives stack and accumulate with AND semantics. For single-value directives like #limit, last-one-wins:
#within(boundary("Germany"))#within(boundary("Bavaria")) // Further narrows — both apply (AND)#limit(200)#limit(100) // Last-one-wins — limit is 100
$$.parks = search(leisure: "park");$$.lakes = search(natural: "water", water: "lake");Directives are most useful with multiple outputs. For single-output queries, just chain methods directly — there’s no benefit to using directives.
Sorting and pagination
Section titled “Sorting and pagination”.sort() — order results
Section titled “.sort() — order results”.sort() takes an expression — the same expression language used in .filter():
// Sort by tag valuesearch(amenity: "school") .within(geometry: $city) .sort(t["name"]);
// Sort by distance from a pointsearch(amenity: "cafe") .around(distance: 1000, geometry: point(48.85, 2.35)) .sort(distance(point(48.85, 2.35)));
// Sort descendingsearch(way, leisure: "park") .within(geometry: $city) .sort(area(), order: :desc);
// Sort by numeric tag valuesearch(node, amenity: "restaurant") .within(geometry: $city) .sort(number(t["capacity"]), order: :desc);
// Quadtile sort (spatial clustering for efficient map rendering)search(way, building: *) .bbox(40.7, -74.0, 40.8, -73.9) .sort(by: :qt);Sort expressions: t["key"] (tag value), distance(point(...)), area(), length(), elevation(), number(t["key"]), id(). Default order is ascending; use order: :desc for descending.
Both positional and keyword forms work: .sort(t["name"]) and .sort(by: t["name"]) are equivalent.
Quadtile sort (:qt) orders results by spatial locality — features near each other on the map are adjacent in the result. This is useful for map rendering and tiling workflows where spatial clustering improves performance.
.limit() — cap result count
Section titled “.limit() — cap result count”search(amenity: "cafe") .around(distance: 500, geometry: point(48.85, 2.35)) .limit(count: 20);.offset() — skip results (pagination)
Section titled “.offset() — skip results (pagination)”// Page 2 (results 21-40)search(amenity: "cafe") .within(geometry: $city) .sort(t["name"]) .limit(count: 20) .offset(skip: 20);Output modes
Section titled “Output modes”By default, queries return full features with geometry and all tags. Output mode methods change what’s returned:
.count() — just the count
Section titled “.count() — just the count”search(amenity: "cafe").within(geometry: $berlin).count();// Returns: { "count": 4521 }.ids() — just IDs
Section titled “.ids() — just IDs”search(amenity: "cafe").within(geometry: $berlin).ids();// Returns: [123456, 789012, ...].tags() — IDs and tags, no geometry
Section titled “.tags() — IDs and tags, no geometry”search(amenity: "cafe").within(geometry: $berlin).tags();.skel() — IDs and geometry, no tags
Section titled “.skel() — IDs and geometry, no tags”search(way, building: *).bbox(40.7, -74.0, 40.8, -73.9).skel();.geom() — full geometry
Section titled “.geom() — full geometry”search(way, building: *).bbox(40.7, -74.0, 40.8, -73.9).geom();.geom() returns full geometry for each feature. This is an alias for the default output behavior — it’s useful when you want to be explicit about requesting geometry, especially in multi-output queries where other outputs use .tags() or .ids().
Output modes are terminal — they must be the last method in the chain, and only one is allowed per output.
Aggregation
Section titled “Aggregation”Aggregation functions compute summary statistics over a result set. They are terminal — like output modes, they end the chain.
.count() with grouping
Section titled “.count() with grouping”Plain .count() returns a single number. Combined with .group_by(), it returns counts per group:
// Total cafes in Berlinsearch(amenity: "cafe").within(geometry: $berlin).count();// Returns: { "count": 4521 }
// Cafes grouped by cuisinesearch(amenity: "cafe") .within(geometry: $berlin) .group_by(t["cuisine"]) .count();// Returns: { "italian": 150, "french": 89, "japanese": 42, ... }.sum() — sum numeric values
Section titled “.sum() — sum numeric values”// Total seating capacity of restaurants in a neighborhoodsearch(amenity: "restaurant") .within(geometry: $neighborhood) .sum(number(t["capacity"]));// Returns: { "sum": 12450 }.avg() — average numeric values
Section titled “.avg() — average numeric values”// Average rating of restaurants by cuisinesearch(amenity: "restaurant") .within(geometry: $city) .group_by(t["cuisine"]) .avg(number(t["rating"]));// Returns: { "italian": 4.2, "japanese": 4.5, "mexican": 3.8, ... }.min() and .max() — extremes
Section titled “.min() and .max() — extremes”// Shortest and tallest buildingssearch(way, building: *) .within(geometry: $downtown) .min(number(t["height"]));
search(way, building: *) .within(geometry: $downtown) .max(number(t["height"]));.group_by() — group before aggregating
Section titled “.group_by() — group before aggregating”.group_by() takes a tag access expression and partitions results before applying an aggregation function:
// Count amenities by typesearch(amenity: *) .within(geometry: $city) .group_by(t["amenity"]) .count();// Returns: { "cafe": 1200, "restaurant": 890, "bar": 450, ... }
// Average building height by typesearch(way, building: *) .within(geometry: $downtown) .group_by(t["building"]) .avg(number(t["height"]));// Returns: { "residential": 12.5, "commercial": 28.3, "industrial": 8.1, ... }Tag values are strings in OSM. Use number() to cast them for arithmetic aggregations like .sum(), .avg(), .min(), and .max(). .count() doesn’t need number() since it just counts elements. See Expressions for details on number() and t["key"].