--- title: Set Operations & Outputs | Plaza Docs description: Combine result sets, produce multiple outputs, and control sorting, pagination, output modes, and aggregation in PlazaQL. --- ## Set operations ### 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 ``` // All cafes except Starbucks search(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 ``` // 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 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 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 `$$.name` once set - No forward references — define `$$.name` before referencing it - Use regular `$var` variables 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 equivalent search(amenity: "cafe").limit(count: 10); $$ = search(amenity: "cafe").limit(count: 10); ``` ## 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 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 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 ### `.sort()` — order results `.sort()` takes an expression — the same expression language used in `.filter()`: ``` // Sort by tag value search(amenity: "school") .within(geometry: $city) .sort(t["name"]); // Sort by distance from a point search(amenity: "cafe") .around(distance: 1000, geometry: point(48.85, 2.35)) .sort(distance(point(48.85, 2.35))); // Sort descending search(way, leisure: "park") .within(geometry: $city) .sort(area(), order: :desc); // Sort by numeric tag value search(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 ``` search(amenity: "cafe") .around(distance: 500, geometry: point(48.85, 2.35)) .limit(count: 20); ``` ### `.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 By default, queries return full features with geometry and all tags. Output mode methods change what’s returned: ### `.count()` — just the count ``` search(amenity: "cafe").within(geometry: $berlin).count(); // Returns: { "count": 4521 } ``` ### `.ids()` — just IDs ``` search(amenity: "cafe").within(geometry: $berlin).ids(); // Returns: [123456, 789012, ...] ``` ### `.tags()` — IDs and tags, no geometry ``` search(amenity: "cafe").within(geometry: $berlin).tags(); ``` ### `.skel()` — IDs and geometry, no tags ``` search(way, building: *).bbox(40.7, -74.0, 40.8, -73.9).skel(); ``` ### `.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 Aggregation functions compute summary statistics over a result set. They are terminal — like output modes, they end the chain. ### `.count()` with grouping Plain `.count()` returns a single number. Combined with `.group_by()`, it returns counts per group: ``` // Total cafes in Berlin search(amenity: "cafe").within(geometry: $berlin).count(); // Returns: { "count": 4521 } // Cafes grouped by cuisine search(amenity: "cafe") .within(geometry: $berlin) .group_by(t["cuisine"]) .count(); // Returns: { "italian": 150, "french": 89, "japanese": 42, ... } ``` ### `.sum()` — sum numeric values ``` // Total seating capacity of restaurants in a neighborhood search(amenity: "restaurant") .within(geometry: $neighborhood) .sum(number(t["capacity"])); // Returns: { "sum": 12450 } ``` ### `.avg()` — average numeric values ``` // Average rating of restaurants by cuisine search(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 ``` // Shortest and tallest buildings search(way, building: *) .within(geometry: $downtown) .min(number(t["height"])); search(way, building: *) .within(geometry: $downtown) .max(number(t["height"])); ``` ### `.group_by()` — group before aggregating `.group_by()` takes a tag access expression and partitions results before applying an aggregation function: ``` // Count amenities by type search(amenity: *) .within(geometry: $city) .group_by(t["amenity"]) .count(); // Returns: { "cafe": 1200, "restaurant": 890, "bar": 450, ... } // Average building height by type search(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](/guides/plazaql/advanced#expressions/index.md) for details on `number()` and `t["key"]`.