← Back to Geo docs

Geo — Tool Reference

Location intelligence on OpenStreetMap data: geocoding via LocationIQ or Geoapify, OSM feature queries, and geometry operations

8 tools · auto-generated from the deployed toolkit

Your AI assistant discovers and invokes these tools through the MCP Server's meta tools — it calls execute_tool with the tool name and arguments below.

# geocode read

Batch geocode up to 25 free-text queries to coordinates and OSM metadata via LocationIQ or Geoapify.

Full description
Geocode free-text queries to coordinates and OpenStreetMap metadata.

Runs up to 25 queries in one call against the provider chosen with
`source` (the matching API key must be configured for the Geo toolkit).
Each match includes lat/lon, a [west, south, east, north] bbox, display
name, OSM type/id, class/type, and an importance score where available.
Set `include_geojson` to get boundary polygons for areas (LocationIQ
only). Pass `near_bbox` to bias ambiguous names toward your region;
bias is soft and can lose to strong global matches, so add
restrict_to_bbox=true when results must fall inside the box (a per-query
`warning` is set when the top match lands far outside the box). LocationIQ matches always carry osm_type/osm_id; Geoapify's API
does not expose them, so prefer source='locationiq' when you need OSM
ids (e.g. to chain into lookup_place). Calls are paced to the provider's
free-tier rate limit; if the provider still returns 429, remaining
queries are skipped and `rate_limited` is set. Results include
OpenStreetMap/provider attribution that must be shown when data is
displayed publicly.
Parameter Type Required Description
queries array<string> Yes Free-text queries to geocode, e.g. '650 Main St, Edmonds, WA'. Up to 25 per call.
source "locationiq" | "geoapify" Yes Geocoding provider to use. Requires that provider's API key to be configured for the Geo toolkit.
limit integer No Maximum matches to return per query. Default: 3
include_geojson boolean No Include boundary polygons as GeoJSON where available. Supported by LocationIQ only; Geoapify matches always omit geometry. Default: false
near_bbox array<number> | null No [west, south, east, north] bias box: prefer matches inside it. Bias is a soft preference and can lose to strong global name matches (especially on LocationIQ); set restrict_to_bbox=true when results must come from your region.
restrict_to_bbox boolean No With near_bbox: exclude matches outside the box entirely instead of just preferring inside. Default: false
Parameter schema (JSON)
{
  "properties": {
    "queries": {
      "description": "Free-text queries to geocode, e.g. '650 Main St, Edmonds, WA'. Up to 25 per call.",
      "items": {
        "type": "string"
      },
      "maxItems": 25,
      "minItems": 1,
      "title": "Queries",
      "type": "array"
    },
    "source": {
      "description": "Geocoding provider to use. Requires that provider's API key to be configured for the Geo toolkit.",
      "enum": [
        "locationiq",
        "geoapify"
      ],
      "title": "Source",
      "type": "string"
    },
    "limit": {
      "default": 3,
      "description": "Maximum matches to return per query.",
      "maximum": 10,
      "minimum": 1,
      "title": "Limit",
      "type": "integer"
    },
    "include_geojson": {
      "default": false,
      "description": "Include boundary polygons as GeoJSON where available. Supported by LocationIQ only; Geoapify matches always omit geometry.",
      "title": "Include Geojson",
      "type": "boolean"
    },
    "near_bbox": {
      "anyOf": [
        {
          "items": {
            "type": "number"
          },
          "maxItems": 4,
          "minItems": 4,
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "[west, south, east, north] bias box: prefer matches inside it. Bias is a soft preference and can lose to strong global name matches (especially on LocationIQ); set restrict_to_bbox=true when results must come from your region.",
      "title": "Near Bbox"
    },
    "restrict_to_bbox": {
      "default": false,
      "description": "With near_bbox: exclude matches outside the box entirely instead of just preferring inside.",
      "title": "Restrict To Bbox",
      "type": "boolean"
    }
  },
  "required": [
    "queries",
    "source"
  ],
  "title": "GeocodeParams",
  "type": "object",
  "additionalProperties": false
}
Result schema (JSON)
{
  "$defs": {
    "GeocodeMatch": {
      "properties": {
        "lat": {
          "title": "Lat",
          "type": "number"
        },
        "lon": {
          "title": "Lon",
          "type": "number"
        },
        "display_name": {
          "title": "Display Name",
          "type": "string"
        },
        "bbox": {
          "anyOf": [
            {
              "items": {
                "type": "number"
              },
              "type": "array"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "[west, south, east, north] in WGS84 degrees.",
          "title": "Bbox"
        },
        "osm_type": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "OSM object type: node, way, or relation.",
          "title": "Osm Type"
        },
        "osm_id": {
          "anyOf": [
            {
              "type": "integer"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Osm Id"
        },
        "category": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "OSM class, e.g. 'place', 'amenity', 'boundary'.",
          "title": "Category"
        },
        "type": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "OSM type within the class, e.g. 'city', 'restaurant'.",
          "title": "Type"
        },
        "importance": {
          "anyOf": [
            {
              "type": "number"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Relative importance score when the provider supplies one.",
          "title": "Importance"
        },
        "address": {
          "additionalProperties": {
            "type": "string"
          },
          "description": "Normalized address components.",
          "title": "Address",
          "type": "object"
        },
        "geojson": {
          "anyOf": [
            {
              "additionalProperties": true,
              "type": "object"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Boundary geometry as GeoJSON when include_geojson is set and available.",
          "title": "Geojson"
        }
      },
      "required": [
        "lat",
        "lon",
        "display_name"
      ],
      "title": "GeocodeMatch",
      "type": "object"
    },
    "GeocodeQueryResult": {
      "properties": {
        "query": {
          "title": "Query",
          "type": "string"
        },
        "matches": {
          "items": {
            "$ref": "#/$defs/GeocodeMatch"
          },
          "title": "Matches",
          "type": "array"
        },
        "error": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Error"
        },
        "warning": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Set when the top match falls far outside near_bbox: the soft bias lost to a global name match. Retry with restrict_to_bbox=true when results must come from that area.",
          "title": "Warning"
        }
      },
      "required": [
        "query"
      ],
      "title": "GeocodeQueryResult",
      "type": "object"
    }
  },
  "properties": {
    "success": {
      "title": "Success",
      "type": "boolean"
    },
    "source": {
      "title": "Source",
      "type": "string"
    },
    "attribution": {
      "title": "Attribution",
      "type": "string"
    },
    "rate_limited": {
      "default": false,
      "description": "True when the provider rate limit was hit and remaining queries were skipped.",
      "title": "Rate Limited",
      "type": "boolean"
    },
    "results": {
      "items": {
        "$ref": "#/$defs/GeocodeQueryResult"
      },
      "title": "Results",
      "type": "array"
    }
  },
  "required": [
    "success",
    "source",
    "attribution",
    "results"
  ],
  "title": "GeocodeResult",
  "type": "object"
}

# lookup_place read

Get full details (tags, centroid, bbox, geometry metadata, and a server-side aoi_ref handle for the boundary/shape — inline GeoJSON on request) for one OSM object by type and id (no key needed).

Full description
Get full details for a single OpenStreetMap object by type and id.

Returns the complete tag set, a representative point, the bounding box,
geometry metadata (type, vertex and ring counts, reported even when the
shape itself is omitted — the ref-first default already returns them
without the inline geometry, so you can gauge payload size before
fetching), area_km2 for polygonal shapes (computed on the geometry as
returned, with a warning when simplification shifted the area noticeably),
and (for ways/relations) the boundary or shape as GeoJSON, assembled from
the object's geometry, suitable as AOI input.
The full-fidelity geometry is also stored server-side and returned as
aoi_ref: pass that handle to the eo tools or geo make_aoi/measure/
query_features instead of inline GeoJSON (refs expire after about 48
hours). By default (include_geometry unset) a non-point shape is ref-first
— the ref comes back with zero inline vertices, the cheapest way to hand a
boundary to another tool; set include_geometry=true to also get the GeoJSON
inline, or false to force it omitted. If the ref cannot be minted the shape
falls back to inline geometry with a note in every mode (including false —
the caller is never left with neither a ref nor geometry, as in make_aoi).
Nodes are points and carry no polygon; a warning says so, and
find_boundary=true additionally searches for a same-named boundary
polygon nearby and returns that object instead (useful for neighborhoods
that exist in OSM only as a labeled point). Use simplify_tolerance_m or
max_points to compress large shapes; max_points is a hard cap, and when
a shape cannot shrink that far cleanly the smallest rings are dropped
(raise max_points to keep small islands). Runs on the Overpass API with automatic
endpoint failover; no API key required. Raw responses are read under a
~64 MB ceiling (before simplification, which happens in-Lambda), so an
object whose raw geometry exceeds it cannot be fetched whole. Results
carry OpenStreetMap
ODbL attribution that must be shown when data is displayed publicly.
Parameter Type Required Description
osm_type "node" | "way" | "relation" Yes OSM object type.
osm_id integer Yes OSM object id.
include_geometry boolean | null No Whether to return the boundary/shape as inline GeoJSON. Tri-state: unset (default) is ref-first — for a non-point shape that mints an aoi_ref the inline geometry is omitted and you chain the shape by passing that aoi_ref into other tools (the zero-vertex path, same contract as make_aoi); true forces the GeoJSON inline; false always omits it. Very large geometries (>50k points) are omitted with a warning regardless. If the aoi_ref cannot be minted (a rare store-write failure), all three modes fall back to inline geometry with a note (including false — the caller must never be left with neither a ref nor geometry, matching make_aoi). Point results (nodes) never mint an aoi_ref and have no polygon geometry: they return neither polygon geometry nor a ref (unset and true still echo the bare Point inline as before).
simplify_tolerance_m number | null No Douglas-Peucker simplify the shape to this tolerance in meters before returning it.
max_points integer No Adaptively simplify the shape to at most this many points (default 2000, visually lossless at city scale; raise it explicitly when you need raw vertices). Default: 2000
find_boundary boolean No When the object resolves to a node (a point with no polygon, common for neighborhoods), also search Overpass for a same-named boundary/place polygon nearby and return that object instead. The warning field reports what happened. Default: false
Parameter schema (JSON)
{
  "properties": {
    "osm_type": {
      "description": "OSM object type.",
      "enum": [
        "node",
        "way",
        "relation"
      ],
      "title": "Osm Type",
      "type": "string"
    },
    "osm_id": {
      "description": "OSM object id.",
      "minimum": 1,
      "title": "Osm Id",
      "type": "integer"
    },
    "include_geometry": {
      "anyOf": [
        {
          "type": "boolean"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Whether to return the boundary/shape as inline GeoJSON. Tri-state: unset (default) is ref-first — for a non-point shape that mints an aoi_ref the inline geometry is omitted and you chain the shape by passing that aoi_ref into other tools (the zero-vertex path, same contract as make_aoi); true forces the GeoJSON inline; false always omits it. Very large geometries (>50k points) are omitted with a warning regardless. If the aoi_ref cannot be minted (a rare store-write failure), all three modes fall back to inline geometry with a note (including false — the caller must never be left with neither a ref nor geometry, matching make_aoi). Point results (nodes) never mint an aoi_ref and have no polygon geometry: they return neither polygon geometry nor a ref (unset and true still echo the bare Point inline as before).",
      "title": "Include Geometry"
    },
    "simplify_tolerance_m": {
      "anyOf": [
        {
          "exclusiveMinimum": 0,
          "maximum": 10000,
          "type": "number"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Douglas-Peucker simplify the shape to this tolerance in meters before returning it.",
      "title": "Simplify Tolerance M"
    },
    "max_points": {
      "default": 2000,
      "description": "Adaptively simplify the shape to at most this many points (default 2000, visually lossless at city scale; raise it explicitly when you need raw vertices).",
      "maximum": 50000,
      "minimum": 8,
      "title": "Max Points",
      "type": "integer"
    },
    "find_boundary": {
      "default": false,
      "description": "When the object resolves to a node (a point with no polygon, common for neighborhoods), also search Overpass for a same-named boundary/place polygon nearby and return that object instead. The warning field reports what happened.",
      "title": "Find Boundary",
      "type": "boolean"
    }
  },
  "required": [
    "osm_type",
    "osm_id"
  ],
  "title": "LookupPlaceParams",
  "type": "object",
  "additionalProperties": false
}
Result schema (JSON)
{
  "properties": {
    "success": {
      "title": "Success",
      "type": "boolean"
    },
    "attribution": {
      "title": "Attribution",
      "type": "string"
    },
    "found": {
      "title": "Found",
      "type": "boolean"
    },
    "osm_type": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Osm Type"
    },
    "osm_id": {
      "anyOf": [
        {
          "type": "integer"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Osm Id"
    },
    "name": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Name"
    },
    "lat": {
      "anyOf": [
        {
          "type": "number"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Representative point (node location or bounds center).",
      "title": "Lat"
    },
    "lon": {
      "anyOf": [
        {
          "type": "number"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Lon"
    },
    "bbox": {
      "anyOf": [
        {
          "items": {
            "type": "number"
          },
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "[west, south, east, north] in WGS84 degrees.",
      "title": "Bbox"
    },
    "tags": {
      "additionalProperties": {
        "type": "string"
      },
      "title": "Tags",
      "type": "object"
    },
    "geometry_type": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Shape type (Point/LineString/Polygon/MultiPolygon), reported even when geojson is omitted.",
      "title": "Geometry Type"
    },
    "geometry_points": {
      "anyOf": [
        {
          "type": "integer"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Vertex count of the geometry as returned (after any simplification); returned even when geojson is omitted (the ref-first default), so you can gauge size before fetching inline.",
      "title": "Geometry Points"
    },
    "geometry_rings": {
      "anyOf": [
        {
          "type": "integer"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Ring count of the geometry as returned (outer rings + holes); max_points is a hard cap, and a tight budget may drop the smallest rings, so compare against the uncompressed count (the ref-first default, or an include_geometry=false probe) to detect dropped islands.",
      "title": "Geometry Rings"
    },
    "area_km2": {
      "anyOf": [
        {
          "type": "number"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Area of the returned geometry for polygonal shapes (after any simplification; include_geometry=false probes report the uncompressed original); a warning is added when simplification shifted it noticeably.",
      "title": "Area Km2"
    },
    "aoi_ref": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Server-side handle for the full-fidelity (unsimplified) geometry. Pass it as aoi_ref to the eo tools or to geo make_aoi/measure/query_features instead of inline GeoJSON; under the ref-first default (or with include_geometry=false) this is the zero-vertex path to chain a boundary. Null for point results (nodes): a Point is not a usable AOI, so no ref is minted. Refs expire after about 48 hours.",
      "title": "Aoi Ref"
    },
    "geojson": {
      "anyOf": [
        {
          "additionalProperties": true,
          "type": "object"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Geojson"
    },
    "warning": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Warning"
    }
  },
  "required": [
    "success",
    "attribution",
    "found"
  ],
  "title": "LookupPlaceResult",
  "type": "object"
}

# make_aoi read

Build and transform GeoJSON areas of interest (point buffers, bbox polygons, buffering, simplify, merge, intersect, difference, boundary-to-line, validate); accepts and returns server-side aoi_ref handles so geometry chains without passing inline.

Full description
Build or transform a GeoJSON area of interest (AOI) with geometry ops.

Operations (all results in WGS84, with area_km2 and vertex_count):
- point_buffer: circle of buffer_m meters around point
- bbox_to_polygon: [west, south, east, north] bbox as a polygon
- buffer_geometry: grow a GeoJSON geometry by buffer_m meters
- simplify: reduce vertex count with tolerance_m meters tolerance (topology-preserving)
- merge: union 2-100 GeoJSON geometries into one (for features from a
  query, geometry='merged' on query_features unions server-side instead)
- intersect: shared region of exactly 2 geometries; a line intersected
  with a polygon yields the line segment(s) inside it, which is how to
  extract a boundary sub-segment (e.g. a park boundary's shoreline:
  boundary_to_line the park, intersect with the buffered water geometry,
  then measure op=length)
- difference: the first of exactly 2 geometries minus the second
- boundary_to_line: a polygon's boundary rings as line geometry, ready
  for intersect or measure op=length
- validate: check a geometry, explain problems, and return a repaired version when invalid

Metric operations run in a local projected plane anchored at the
geometry, so distances are meters, not degrees; accuracy is ~1% for AOIs
up to a few tens of km, degrading with size and latitude. Geometries
spanning the antimeridian or within 0.1 degrees of the poles are
rejected. Runs entirely in-Lambda; no external calls, no API key. Output
GeoJSON is directly usable as an AOI for other geo tools and
earth-observation workflows.

Geometry inputs also accept stored AOI handles: pass geometry_ref (or
geometry_refs) instead of geometry (geometries) to operate on the
full-fidelity shape behind an aoi_ref from lookup_place, query_features,
or an earlier make_aoi. Every result is stored server-side and returned
with its own aoi_ref, so buffer/merge/intersect chains stay ref-in,
ref-out; whenever a ref was stored the inline geojson is omitted by
default (set return_geometry=true for inline geometry). Refs expire
after about 48 hours.
Parameter Type Required Description
op "point_buffer" | "bbox_to_polygon" | "buffer_geometry" | "simplify" | "merge" | "intersect" | "difference" | "boundary_to_line" | "validate" Yes Geometry operation to perform.
point Coordinate | null No Center point (required for point_buffer).
buffer_m number | null No Buffer distance in meters (required for point_buffer and buffer_geometry).
bbox array<number> | null No [west, south, east, north] in WGS84 degrees (required for bbox_to_polygon).
geometry object | null No GeoJSON geometry (required for buffer_geometry, simplify, boundary_to_line, validate). Alternatively pass geometry_ref.
geometry_ref string | null No Stored AOI handle (e.g. from lookup_place or an earlier make_aoi) to use instead of inline geometry; the full-fidelity stored shape is operated on, so ref-in/ref-out chains never pass coordinates through the conversation. Refs expire after about 48 hours. Mutually exclusive with geometry.
geometries array<object> | null No GeoJSON geometries (required for merge: 2-100 to union; intersect/difference: exactly 2). Alternatively pass geometry_refs.
geometry_refs array<string> | null No Stored AOI handles to use instead of inline geometries (merge: 2-100; intersect/difference: exactly 2). Mutually exclusive with geometries.
tolerance_m number | null No Simplification tolerance in meters (required for simplify).
return_geometry boolean | null No Inline geometry control: true always returns geojson; default (unset) and false omit it whenever an aoi_ref was stored (ref-first), falling back to inline geometry only when no ref could be stored. Metadata (area_km2, vertex_count, bbox) is always returned.
Parameter schema (JSON)
{
  "$defs": {
    "Coordinate": {
      "properties": {
        "lat": {
          "description": "Latitude in WGS84 decimal degrees.",
          "maximum": 90,
          "minimum": -90,
          "title": "Lat",
          "type": "number"
        },
        "lon": {
          "description": "Longitude in WGS84 decimal degrees.",
          "maximum": 180,
          "minimum": -180,
          "title": "Lon",
          "type": "number"
        }
      },
      "required": [
        "lat",
        "lon"
      ],
      "title": "Coordinate",
      "type": "object",
      "additionalProperties": false
    }
  },
  "properties": {
    "op": {
      "description": "Geometry operation to perform.",
      "enum": [
        "point_buffer",
        "bbox_to_polygon",
        "buffer_geometry",
        "simplify",
        "merge",
        "intersect",
        "difference",
        "boundary_to_line",
        "validate"
      ],
      "title": "Op",
      "type": "string"
    },
    "point": {
      "anyOf": [
        {
          "$ref": "#/$defs/Coordinate"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Center point (required for point_buffer)."
    },
    "buffer_m": {
      "anyOf": [
        {
          "exclusiveMinimum": 0,
          "maximum": 100000,
          "type": "number"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Buffer distance in meters (required for point_buffer and buffer_geometry).",
      "title": "Buffer M"
    },
    "bbox": {
      "anyOf": [
        {
          "items": {
            "type": "number"
          },
          "maxItems": 4,
          "minItems": 4,
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "[west, south, east, north] in WGS84 degrees (required for bbox_to_polygon).",
      "title": "Bbox"
    },
    "geometry": {
      "anyOf": [
        {
          "additionalProperties": true,
          "type": "object"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "GeoJSON geometry (required for buffer_geometry, simplify, boundary_to_line, validate). Alternatively pass geometry_ref.",
      "title": "Geometry"
    },
    "geometry_ref": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Stored AOI handle (e.g. from lookup_place or an earlier make_aoi) to use instead of inline geometry; the full-fidelity stored shape is operated on, so ref-in/ref-out chains never pass coordinates through the conversation. Refs expire after about 48 hours. Mutually exclusive with geometry.",
      "title": "Geometry Ref"
    },
    "geometries": {
      "anyOf": [
        {
          "items": {
            "additionalProperties": true,
            "type": "object"
          },
          "maxItems": 100,
          "minItems": 2,
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "GeoJSON geometries (required for merge: 2-100 to union; intersect/difference: exactly 2). Alternatively pass geometry_refs.",
      "title": "Geometries"
    },
    "geometry_refs": {
      "anyOf": [
        {
          "items": {
            "type": "string"
          },
          "maxItems": 100,
          "minItems": 2,
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Stored AOI handles to use instead of inline geometries (merge: 2-100; intersect/difference: exactly 2). Mutually exclusive with geometries.",
      "title": "Geometry Refs"
    },
    "tolerance_m": {
      "anyOf": [
        {
          "exclusiveMinimum": 0,
          "maximum": 10000,
          "type": "number"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Simplification tolerance in meters (required for simplify).",
      "title": "Tolerance M"
    },
    "return_geometry": {
      "anyOf": [
        {
          "type": "boolean"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Inline geometry control: true always returns geojson; default (unset) and false omit it whenever an aoi_ref was stored (ref-first), falling back to inline geometry only when no ref could be stored. Metadata (area_km2, vertex_count, bbox) is always returned.",
      "title": "Return Geometry"
    }
  },
  "required": [
    "op"
  ],
  "title": "MakeAoiParams",
  "type": "object",
  "additionalProperties": false
}
Result schema (JSON)
{
  "properties": {
    "success": {
      "title": "Success",
      "type": "boolean"
    },
    "op": {
      "title": "Op",
      "type": "string"
    },
    "geojson": {
      "anyOf": [
        {
          "additionalProperties": true,
          "type": "object"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Resulting geometry as GeoJSON (WGS84). Omitted whenever an aoi_ref was stored unless return_geometry=true; chain via aoi_ref instead.",
      "title": "Geojson"
    },
    "aoi_ref": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Server-side handle for the full result geometry. Pass it as aoi_ref/geometry_ref to the eo tools or other geo tools instead of inline GeoJSON; refs expire after about 48 hours.",
      "title": "Aoi Ref"
    },
    "area_km2": {
      "anyOf": [
        {
          "type": "number"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Area Km2"
    },
    "vertex_count": {
      "anyOf": [
        {
          "type": "integer"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Vertex Count"
    },
    "bbox": {
      "anyOf": [
        {
          "items": {
            "type": "number"
          },
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "[west, south, east, north] of the result geometry in WGS84 degrees.",
      "title": "Bbox"
    },
    "note": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Caveat about the result, e.g. why geojson was omitted.",
      "title": "Note"
    },
    "valid": {
      "anyOf": [
        {
          "type": "boolean"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Set by op=validate: whether the input geometry is valid.",
      "title": "Valid"
    },
    "validity_reason": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Set by op=validate when invalid: what is wrong and where.",
      "title": "Validity Reason"
    }
  },
  "required": [
    "success",
    "op"
  ],
  "title": "AoiResult",
  "type": "object"
}

# measure read

Measure great-circle distance along a path, geometry area, line length, centroid, or point-in-polygon containment on inline GeoJSON, an OSM reference, or a stored aoi_ref.

Full description
Measure distances, areas, lengths, centroids, and containment on WGS84 geometry.

- distance: great-circle (haversine, spherical-earth ~0.3% accuracy)
  length along a path of 2-100 points — straight-line, not travel
  distance; use the route tool for routed walk/drive/bike distance
- area: area in km² and m² plus perimeter_km (local projected plane;
  antimeridian-spanning and near-polar geometry rejected)
- length: length of LineString/MultiLineString geometry (e.g. a
  boundary segment from make_aoi boundary_to_line + intersect), or
  boundary length of polygonal geometry
- centroid: planar centroid in lat/lon degree space; can fall outside
  concave or multi-part shapes
- contains: whether a geometry covers a point (boundary counts)

Geometry ops accept inline GeoJSON, an OSM reference (osm_type +
osm_id), or a stored AOI handle (aoi_ref from lookup_place, make_aoi,
or query_features; refs expire after about 48 hours): with a reference
the shape is resolved server-side and only the numbers return, so large
boundaries never pass through the conversation ("how big is this park"
is one small call). Pass
simplify_tolerance_m to measure a simplified version of the shape —
comparing original vs simplified area is two small calls, no GeoJSON
shuttling. Geometry math runs in-Lambda; references fetch from the
Overpass API under a ~64 MB raw-response ceiling (applied before any
simplification), so an object larger than that cannot be measured by
reference.
Parameter Type Required Description
op "distance" | "area" | "length" | "centroid" | "contains" Yes Measurement to perform.
points array<Coordinate> | null No Path of points for op=distance (great-circle, summed leg by leg).
geometry object | null No GeoJSON geometry (for area, centroid, contains). Alternatively pass osm_type + osm_id.
osm_type "node" | "way" | "relation" | null No With osm_id: measure an OSM object directly by reference; its shape is fetched server-side, so no GeoJSON needs to pass through the conversation.
osm_id integer | null No OSM object id (with osm_type).
aoi_ref string | null No Stored AOI handle from lookup_place, make_aoi, or query_features to measure instead of inline geometry; the full-fidelity stored shape is used and no GeoJSON passes through the conversation. Refs expire after about 48 hours. Pass exactly one of geometry, osm_type + osm_id, or aoi_ref.
point Coordinate | null No Test point for op=contains.
simplify_tolerance_m number | null No Douglas-Peucker simplify the geometry (topology-preserving) before measuring. With an OSM reference this compares original vs simplified in two small calls without any GeoJSON passing through the conversation.
Parameter schema (JSON)
{
  "$defs": {
    "Coordinate": {
      "properties": {
        "lat": {
          "description": "Latitude in WGS84 decimal degrees.",
          "maximum": 90,
          "minimum": -90,
          "title": "Lat",
          "type": "number"
        },
        "lon": {
          "description": "Longitude in WGS84 decimal degrees.",
          "maximum": 180,
          "minimum": -180,
          "title": "Lon",
          "type": "number"
        }
      },
      "required": [
        "lat",
        "lon"
      ],
      "title": "Coordinate",
      "type": "object",
      "additionalProperties": false
    }
  },
  "properties": {
    "op": {
      "description": "Measurement to perform.",
      "enum": [
        "distance",
        "area",
        "length",
        "centroid",
        "contains"
      ],
      "title": "Op",
      "type": "string"
    },
    "points": {
      "anyOf": [
        {
          "items": {
            "$ref": "#/$defs/Coordinate"
          },
          "maxItems": 100,
          "minItems": 2,
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Path of points for op=distance (great-circle, summed leg by leg).",
      "title": "Points"
    },
    "geometry": {
      "anyOf": [
        {
          "additionalProperties": true,
          "type": "object"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "GeoJSON geometry (for area, centroid, contains). Alternatively pass osm_type + osm_id.",
      "title": "Geometry"
    },
    "osm_type": {
      "anyOf": [
        {
          "enum": [
            "node",
            "way",
            "relation"
          ],
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "With osm_id: measure an OSM object directly by reference; its shape is fetched server-side, so no GeoJSON needs to pass through the conversation.",
      "title": "Osm Type"
    },
    "osm_id": {
      "anyOf": [
        {
          "minimum": 1,
          "type": "integer"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "OSM object id (with osm_type).",
      "title": "Osm Id"
    },
    "aoi_ref": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Stored AOI handle from lookup_place, make_aoi, or query_features to measure instead of inline geometry; the full-fidelity stored shape is used and no GeoJSON passes through the conversation. Refs expire after about 48 hours. Pass exactly one of geometry, osm_type + osm_id, or aoi_ref.",
      "title": "Aoi Ref"
    },
    "point": {
      "anyOf": [
        {
          "$ref": "#/$defs/Coordinate"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Test point for op=contains."
    },
    "simplify_tolerance_m": {
      "anyOf": [
        {
          "exclusiveMinimum": 0,
          "maximum": 10000,
          "type": "number"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Douglas-Peucker simplify the geometry (topology-preserving) before measuring. With an OSM reference this compares original vs simplified in two small calls without any GeoJSON passing through the conversation.",
      "title": "Simplify Tolerance M"
    }
  },
  "required": [
    "op"
  ],
  "title": "MeasureParams",
  "type": "object",
  "additionalProperties": false
}
Result schema (JSON)
{
  "properties": {
    "success": {
      "title": "Success",
      "type": "boolean"
    },
    "op": {
      "title": "Op",
      "type": "string"
    },
    "distance_m": {
      "anyOf": [
        {
          "type": "number"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Distance M"
    },
    "distance_km": {
      "anyOf": [
        {
          "type": "number"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Distance Km"
    },
    "area_km2": {
      "anyOf": [
        {
          "type": "number"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Area Km2"
    },
    "area_m2": {
      "anyOf": [
        {
          "type": "number"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Area M2"
    },
    "perimeter_km": {
      "anyOf": [
        {
          "type": "number"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Boundary length for op=area, including holes and islands.",
      "title": "Perimeter Km"
    },
    "length_km": {
      "anyOf": [
        {
          "type": "number"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "For op=length: line length of linear geometry, or boundary length of polygonal geometry.",
      "title": "Length Km"
    },
    "length_m": {
      "anyOf": [
        {
          "type": "number"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Length M"
    },
    "lat": {
      "anyOf": [
        {
          "type": "number"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Centroid latitude for op=centroid.",
      "title": "Lat"
    },
    "lon": {
      "anyOf": [
        {
          "type": "number"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Centroid longitude for op=centroid.",
      "title": "Lon"
    },
    "contains": {
      "anyOf": [
        {
          "type": "boolean"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Whether the geometry covers the point (boundary counts as inside).",
      "title": "Contains"
    }
  },
  "required": [
    "success",
    "op"
  ],
  "title": "MeasureResult",
  "type": "object"
}

# query_features read

Find OpenStreetMap features by tag within a radius, bbox, named area, boundary referenced by OSM id, or stored aoi_ref via the Overpass API (no key needed).

Full description
Find OpenStreetMap features matching tag filters in a bounded area.

Tag filters are AND-combined (up to 6), e.g. leisure=pitch + sport=tennis.
Pass them as `tags` for a single AND-group. When one concept spans
several OSM keys, pass `any_of` instead: OR-combined groups (up to 4,
each an AND-group) unioned in one query and deduped server-side, so
"ice cream shops" (amenity=ice_cream OR cuisine=ice_cream OR
shop=ice_cream) is a single call rather than three queries merged
client-side: any_of=[[{key:'amenity',value:'ice_cream'}],
[{key:'cuisine',value:'ice_cream'}], [{key:'shop',value:'ice_cream'}]].
Provide exactly one of `tags` or `any_of`.
Center+radius results carry distance_m and are sorted nearest-first
(within the returned page; if `truncated` is set, narrow the radius for
a guaranteed nearest-N).
Values are checked against real OSM vocabulary: common misnomers are
auto-corrected (leisure=tennis_court is interpreted as leisure=pitch +
sport=tennis and reported in `note`), unknown values for well-known keys
fail fast with suggestions, and multi-value keys like sport match
semicolon lists (sport=tennis;basketball). Search within exactly one of:
center + radius_m (max 30km), bbox [west, south, east, north] (max
~2500 km²), area_name (an exact, case-sensitive OSM area name like
'Edmonds', resolved by Overpass itself, so no geocoding key is required;
the place must exist as an OSM area, i.e. a boundary relation or closed
way), area_osm_type + area_osm_id (search inside one specific
boundary by OSM reference — the reliable path for "all X in <city>":
geocode the city or find its admin boundary, then pass its relation id
here; names collide across the planet, ids do not), or aoi_ref (a
stored AOI handle from lookup_place, make_aoi, or an earlier merged
query; the polygon never passes through the conversation, and refs
expire after about 48 hours).
Runs on the Overpass API with automatic endpoint failover. Raw responses
are read under a ~64 MB ceiling; a valid query too large to hold fails
with guidance to lower limit, narrow the area, or use geometry='center'.
geometry='center' (default) returns one point per feature;
geometry='full' adds real geometry as GeoJSON, suitable as AOI input;
geometry='merged' unions all matched polygonal features server-side
into one merged_geojson AOI (with merged_area_km2 and
merged_vertex_count) while individual features stay compact — "all parks in a city as one buffered AOI" is
then one query plus one make_aoi buffer_geometry call, with no
geometry shuttling through the conversation; the unsimplified union is
also stored server-side and returned as merged_aoi_ref, and whenever
that ref was stored the inline merged_geojson is omitted by default
(set return_geometry=true for inline geometry). include_area=true adds
area_km2 per polygonal feature in any mode, so "largest park in X" is
one call. Use simplify_tolerance_m
or max_points_per_feature to compress large shapes (lakes, boundaries)
without losing their form. Restricted
features (access=private/no) are included with their access tag
visible; filter by that tag when the user wants visitable places, or
set exclude_private=true. Results carry OpenStreetMap ODbL attribution
that must be shown when data is displayed publicly.
Parameter Type Required Description
tags array<TagFilter> | null No Tag filters, AND-combined. E.g. [{key: 'leisure', value: 'pitch'}, {key: 'sport', value: 'tennis'}]. Provide exactly one of `tags` (a single AND-group) or `any_of` (OR-combined groups); use `any_of` when the concept spans multiple tag keys.
any_of array<array<TagFilter>> | null No OR-combined tag groups (max 4 groups, each up to 6 AND-combined filters). Use when one concept is modeled under different OSM keys and a single AND-list cannot express it — the groups are unioned in a single Overpass query and results are deduped server-side, replacing several separate queries plus client-side merging. Each group is resolved independently (aliases, vocabulary checks, multi-value matching). E.g. ice cream shops span three keys: any_of=[[{key: 'amenity', value: 'ice_cream'}], [{key: 'cuisine', value: 'ice_cream'}], [{key: 'shop', value: 'ice_cream'}]]. Mutually exclusive with `tags`.
center Coordinate | null No Search center; requires radius_m. Exactly one of center+radius_m, bbox, or area_name.
radius_m integer | null No Search radius in meters around center (max 30km).
bbox array<number> | null No [west, south, east, north] in WGS84 degrees; max ~2500 km².
area_name string | null No Search inside the OSM area with this exact name (e.g. 'Edmonds'), resolved by Overpass itself; no geocoding key needed. Names are globally ambiguous — every same-named area is searched (reported in the note), and a name matching more than 200 is refused rather than searched in part — so prefer area_osm_type + area_osm_id when you hold the boundary's OSM reference.
area_osm_type "way" | "relation" | null No With area_osm_id: search inside one specific OSM area by reference (e.g. an admin boundary relation from geocode or an earlier query). Unambiguous, unlike area_name.
area_osm_id integer | null No OSM object id of the boundary to search inside (with area_osm_type).
aoi_ref string | null No Search inside a stored AOI: pass the `aoi_ref`/`merged_aoi_ref` handle returned by lookup_place, make_aoi, or query_features instead of inline GeoJSON. Must resolve to a Polygon/MultiPolygon; refs expire after about 48 hours. Exactly one area selector.
exclude_private boolean No Server-side exclusion of features tagged access=private/no. Off by default: results include restricted features with their access tag visible, so filter contextually (e.g. drop access=private when the user wants places they can visit). Set true to exclude them outright. Default: false
geometry "center" | "full" | "merged" No 'center' returns one point per feature (compact); 'full' adds actual geometry as GeoJSON for AOI use; 'merged' unions all matched polygonal features server-side into one AOI returned as merged_aoi_ref (with merged_area_km2 and merged_vertex_count; inline merged_geojson only with return_geometry=true or when no ref could be stored) while individual features stay compact — the zero-shuttle path for 'all X in an area as one AOI'. Default: "center"
include_area boolean No Compute area_km2 per polygonal feature (null for nodes/lines), so size questions like 'largest park' are one call. Fetches geometry server-side even with geometry='center'; the response stays compact. Default: false
limit integer No Maximum features to return. Default: 50
simplify_tolerance_m number | null No With geometry='full': Douglas-Peucker simplify each shape to this tolerance in meters (10-25 keeps shapes visually intact at ~10-20% of the points).
max_points_per_feature integer No With geometry='full': adaptively simplify each shape to at most this many points. Default 100 keeps multi-feature responses renderable; for one precise boundary raise this, or take the osm_id to lookup_place (default 2000) or measure. Default: 100
return_geometry boolean | null No With geometry='merged': controls the inline merged_geojson. true always returns it; default (unset) and false omit it whenever merged_aoi_ref was stored (ref-first), falling back to inline geometry only when no ref could be stored. Metadata (merged_area_km2, merged_vertex_count, merged_aoi_ref) is always returned.
Parameter schema (JSON)
{
  "$defs": {
    "Coordinate": {
      "properties": {
        "lat": {
          "description": "Latitude in WGS84 decimal degrees.",
          "maximum": 90,
          "minimum": -90,
          "title": "Lat",
          "type": "number"
        },
        "lon": {
          "description": "Longitude in WGS84 decimal degrees.",
          "maximum": 180,
          "minimum": -180,
          "title": "Lon",
          "type": "number"
        }
      },
      "required": [
        "lat",
        "lon"
      ],
      "title": "Coordinate",
      "type": "object",
      "additionalProperties": false
    },
    "TagFilter": {
      "properties": {
        "key": {
          "description": "OSM tag key, e.g. 'leisure', 'amenity', 'sport'.",
          "minLength": 1,
          "title": "Key",
          "type": "string"
        },
        "value": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "OSM tag value, e.g. 'tennis'. Omit to match any feature that has the key. Common natural-language misnomers are auto-corrected (e.g. leisure=tennis_court becomes leisure=pitch + sport=tennis, amenity=gas_station becomes amenity=fuel) and reported in the response note; unknown values for well-known keys fail fast with suggestions.",
          "title": "Value"
        }
      },
      "required": [
        "key"
      ],
      "title": "TagFilter",
      "type": "object",
      "additionalProperties": false
    }
  },
  "properties": {
    "tags": {
      "anyOf": [
        {
          "items": {
            "$ref": "#/$defs/TagFilter"
          },
          "maxItems": 6,
          "minItems": 1,
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Tag filters, AND-combined. E.g. [{key: 'leisure', value: 'pitch'}, {key: 'sport', value: 'tennis'}]. Provide exactly one of `tags` (a single AND-group) or `any_of` (OR-combined groups); use `any_of` when the concept spans multiple tag keys.",
      "title": "Tags"
    },
    "any_of": {
      "anyOf": [
        {
          "items": {
            "items": {
              "$ref": "#/$defs/TagFilter"
            },
            "type": "array"
          },
          "maxItems": 4,
          "minItems": 1,
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "OR-combined tag groups (max 4 groups, each up to 6 AND-combined filters). Use when one concept is modeled under different OSM keys and a single AND-list cannot express it — the groups are unioned in a single Overpass query and results are deduped server-side, replacing several separate queries plus client-side merging. Each group is resolved independently (aliases, vocabulary checks, multi-value matching). E.g. ice cream shops span three keys: any_of=[[{key: 'amenity', value: 'ice_cream'}], [{key: 'cuisine', value: 'ice_cream'}], [{key: 'shop', value: 'ice_cream'}]]. Mutually exclusive with `tags`.",
      "title": "Any Of"
    },
    "center": {
      "anyOf": [
        {
          "$ref": "#/$defs/Coordinate"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Search center; requires radius_m. Exactly one of center+radius_m, bbox, or area_name."
    },
    "radius_m": {
      "anyOf": [
        {
          "maximum": 30000,
          "minimum": 1,
          "type": "integer"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Search radius in meters around center (max 30km).",
      "title": "Radius M"
    },
    "bbox": {
      "anyOf": [
        {
          "items": {
            "type": "number"
          },
          "maxItems": 4,
          "minItems": 4,
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "[west, south, east, north] in WGS84 degrees; max ~2500 km².",
      "title": "Bbox"
    },
    "area_name": {
      "anyOf": [
        {
          "minLength": 1,
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Search inside the OSM area with this exact name (e.g. 'Edmonds'), resolved by Overpass itself; no geocoding key needed. Names are globally ambiguous — every same-named area is searched (reported in the note), and a name matching more than 200 is refused rather than searched in part — so prefer area_osm_type + area_osm_id when you hold the boundary's OSM reference.",
      "title": "Area Name"
    },
    "area_osm_type": {
      "anyOf": [
        {
          "enum": [
            "way",
            "relation"
          ],
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "With area_osm_id: search inside one specific OSM area by reference (e.g. an admin boundary relation from geocode or an earlier query). Unambiguous, unlike area_name.",
      "title": "Area Osm Type"
    },
    "area_osm_id": {
      "anyOf": [
        {
          "minimum": 1,
          "type": "integer"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "OSM object id of the boundary to search inside (with area_osm_type).",
      "title": "Area Osm Id"
    },
    "aoi_ref": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Search inside a stored AOI: pass the `aoi_ref`/`merged_aoi_ref` handle returned by lookup_place, make_aoi, or query_features instead of inline GeoJSON. Must resolve to a Polygon/MultiPolygon; refs expire after about 48 hours. Exactly one area selector.",
      "title": "Aoi Ref"
    },
    "exclude_private": {
      "default": false,
      "description": "Server-side exclusion of features tagged access=private/no. Off by default: results include restricted features with their access tag visible, so filter contextually (e.g. drop access=private when the user wants places they can visit). Set true to exclude them outright.",
      "title": "Exclude Private",
      "type": "boolean"
    },
    "geometry": {
      "default": "center",
      "description": "'center' returns one point per feature (compact); 'full' adds actual geometry as GeoJSON for AOI use; 'merged' unions all matched polygonal features server-side into one AOI returned as merged_aoi_ref (with merged_area_km2 and merged_vertex_count; inline merged_geojson only with return_geometry=true or when no ref could be stored) while individual features stay compact — the zero-shuttle path for 'all X in an area as one AOI'.",
      "enum": [
        "center",
        "full",
        "merged"
      ],
      "title": "Geometry",
      "type": "string"
    },
    "include_area": {
      "default": false,
      "description": "Compute area_km2 per polygonal feature (null for nodes/lines), so size questions like 'largest park' are one call. Fetches geometry server-side even with geometry='center'; the response stays compact.",
      "title": "Include Area",
      "type": "boolean"
    },
    "limit": {
      "default": 50,
      "description": "Maximum features to return.",
      "maximum": 200,
      "minimum": 1,
      "title": "Limit",
      "type": "integer"
    },
    "simplify_tolerance_m": {
      "anyOf": [
        {
          "exclusiveMinimum": 0,
          "maximum": 10000,
          "type": "number"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "With geometry='full': Douglas-Peucker simplify each shape to this tolerance in meters (10-25 keeps shapes visually intact at ~10-20% of the points).",
      "title": "Simplify Tolerance M"
    },
    "max_points_per_feature": {
      "default": 100,
      "description": "With geometry='full': adaptively simplify each shape to at most this many points. Default 100 keeps multi-feature responses renderable; for one precise boundary raise this, or take the osm_id to lookup_place (default 2000) or measure.",
      "maximum": 50000,
      "minimum": 8,
      "title": "Max Points Per Feature",
      "type": "integer"
    },
    "return_geometry": {
      "anyOf": [
        {
          "type": "boolean"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "With geometry='merged': controls the inline merged_geojson. true always returns it; default (unset) and false omit it whenever merged_aoi_ref was stored (ref-first), falling back to inline geometry only when no ref could be stored. Metadata (merged_area_km2, merged_vertex_count, merged_aoi_ref) is always returned.",
      "title": "Return Geometry"
    }
  },
  "title": "QueryFeaturesParams",
  "type": "object",
  "additionalProperties": false
}
Result schema (JSON)
{
  "$defs": {
    "Feature": {
      "properties": {
        "osm_type": {
          "title": "Osm Type",
          "type": "string"
        },
        "osm_id": {
          "title": "Osm Id",
          "type": "integer"
        },
        "name": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Name"
        },
        "lat": {
          "anyOf": [
            {
              "type": "number"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Representative point (node location or bounds center).",
          "title": "Lat"
        },
        "lon": {
          "anyOf": [
            {
              "type": "number"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Lon"
        },
        "distance_m": {
          "anyOf": [
            {
              "type": "number"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Great-circle distance from the query center in meters (center+radius queries only).",
          "title": "Distance M"
        },
        "area_km2": {
          "anyOf": [
            {
              "type": "number"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Feature area with include_area=true; null for non-polygonal features.",
          "title": "Area Km2"
        },
        "tags": {
          "additionalProperties": {
            "type": "string"
          },
          "title": "Tags",
          "type": "object"
        },
        "geojson": {
          "anyOf": [
            {
              "additionalProperties": true,
              "type": "object"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Feature geometry as GeoJSON when geometry='full'.",
          "title": "Geojson"
        }
      },
      "required": [
        "osm_type",
        "osm_id"
      ],
      "title": "Feature",
      "type": "object"
    }
  },
  "properties": {
    "success": {
      "title": "Success",
      "type": "boolean"
    },
    "attribution": {
      "title": "Attribution",
      "type": "string"
    },
    "count": {
      "title": "Count",
      "type": "integer"
    },
    "truncated": {
      "default": false,
      "description": "True when more matches exist than `limit`; narrow the area or raise the limit.",
      "title": "Truncated",
      "type": "boolean"
    },
    "note": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Caveat about the result, e.g. when an area_name search returns nothing.",
      "title": "Note"
    },
    "merged_geojson": {
      "anyOf": [
        {
          "additionalProperties": true,
          "type": "object"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "With geometry='merged': union of all matched polygonal features as one AOI (compressed to ~2000 points), ready for make_aoi buffer_geometry or measure. Omitted by default whenever merged_aoi_ref was stored; set return_geometry=true to include it.",
      "title": "Merged Geojson"
    },
    "merged_area_km2": {
      "anyOf": [
        {
          "type": "number"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Area of the merged union in km².",
      "title": "Merged Area Km2"
    },
    "merged_vertex_count": {
      "anyOf": [
        {
          "type": "integer"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Vertex count of merged_geojson as returned (after compression) when inline geometry is included; when merged_geojson is omitted, the vertex count of the full-fidelity merged union (the geometry behind merged_aoi_ref when a ref was stored).",
      "title": "Merged Vertex Count"
    },
    "merged_aoi_ref": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "With geometry='merged': server-side handle for the unsimplified merged union. Pass it as aoi_ref to eo tools or geo make_aoi/measure/query_features instead of inline GeoJSON; refs expire after about 48 hours.",
      "title": "Merged Aoi Ref"
    },
    "features": {
      "items": {
        "$ref": "#/$defs/Feature"
      },
      "title": "Features",
      "type": "array"
    }
  },
  "required": [
    "success",
    "attribution",
    "count",
    "features"
  ],
  "title": "QueryFeaturesResult",
  "type": "object"
}

# render_outline read

Render OpenStreetMap boundaries/shapes to a hosted outline map (SVG/PNG) via features, a server-side query, or a set_ref replaying an already-resolved set; spotlight='each' batches one image per feature in a single call; optional marker mode draws point features and too-small-to-see shapes as fixed-size symbols; no API key, no PU cost, hosted URL (see expires_at).

Full description
Turn OSM-derived (or inline) geometry into a hosted outline map — a
vector SVG (default) and/or a PNG — without any geometry passing through
the conversation. No API key and no processing-unit cost.

Three modes (provide exactly one). features: an explicit list (max 100) of
shapes, each given as aoi_ref, osm_type + osm_id, or inline geojson, with
optional per-feature stroke/fill (hex colours only), stroke_width, label,
marker, and spotlight. query: resolve-and-render server-side — tag filters
(tags or any_of) inside one area (area_name, area_osm_type + area_osm_id,
or aoi_ref), like query_features, then draw every match. Query mode is
capped at max_features matched OSM objects (default 200, max 1000); over
the cap it fails fast with the exact match count and renders nothing — no
partial maps. That cap bounds how many objects match, not how much
geometry they carry: very heavy boundaries (county or state outlines) can
run out of fetch budget well under the cap, in which case the error says
how many were fetched and what to reduce (smaller area, more tag filters,
lower max_features, or several smaller renders). set_ref: replay a set
this tool already resolved.

Set refs (for repeated renders of the same features). Every features/query
render returns set_ref, a handle for that resolved, assembled set. Pass it
back as set_ref instead of the whole list: no OpenStreetMap round-trip, no
geometry in the conversation, so a 66-card deck is 66 short calls rather
than 66 resends. Per-call options still apply — output, width, projection,
labels, label_scale, attribution_caption, simplify_tolerance_m (coarsening
only) — so the same set can be replayed at a different label size — and
spotlight_indices picks which stored features are spotlit by 0-based
position, REPLACING the set's stored flags ([] means no spotlight,
omitting it keeps the stored ones). Per-feature colours, labels and
markers are fixed when the set is minted; re-render from features to
restyle (marker_default is refused with set_ref for that reason). A replay
echoes the same set_ref without rewriting it, and the response's per-feature
list is suppressed by default (echo_features) because you already have it.
A set_ref is not an aoi_ref and only goes in this slot; sets expire after
about 48 hours, after which re-render from features or query.

Batch spotlight (for decks). spotlight="each" returns ONE IMAGE PER FEATURE
from a single call: N entries in `images`, each spotlighting a different
feature against the dimmed remainder, all sharing one canvas and framing
because the set is resolved, projected and fitted once. Every entry carries
that feature's own name, OSM id, URLs and a `filename` slugified
server-side from that same name — use it verbatim, so a caption can never
end up on the wrong picture. The entry each card is ABOUT is not a promise
that the feature is visible on it: in marker mode a symbol the placement
pass cannot fit is drawn on no image at all, so its own card shows only the
dimmed remainder and reports images[].marker_shown=false. Read that field
before captioning; the note calls those cards out separately. N is capped at 100 IMAGES per call; an
over-cap batch fails fast with the exact count and renders nothing. It
works in all three modes, and set_ref + spotlight="each" is a whole deck in
one short call with nothing resent. spotlight_indices narrows a batch to
those positions — that is how a set bigger than the cap is rendered a slice
at a time, and how a batch that ran out of wall-clock is continued. A batch
is never silently truncated: it returns the images it did finish plus a note
saying how many, why it stopped and what is left. When set_ref is non-null
that note spells out the continuation call in full — including every
per-call option that was in effect (output, width, projection, labels,
label_scale, simplify_tolerance_m, attribution_caption), because all of
them are re-decided per call: output picks which artifacts exist,
attribution_caption whether the PNG carries a caption, and the rest feed
the canvas pixels that label placement and the auto marker/footprint
threshold are measured in. The options are printed as you REQUESTED them,
not as this call computed them — notably `width`, which is a maximum the
height cap can lower for a tall geometry, so the printed value is the one
that re-runs the same fit rather than the narrower result field.
Repeat the call as printed and the
remaining cards match the ones you have. If the set could NOT be stored,
the note instead names the unrendered features, prints `marker_default`
alongside the other options (a resend resolves and styles from scratch, so
the request-level marker default must be repeated — a set_ref replay is the
opposite case, where it is baked in and refused), and is explicit that the
remainder is not pixel-exact: in features mode resending just those
features (named by their position in the features[] list you sent) brings
back the same features, but the canvas is re-fitted to that smaller set, so
framing, which labels fit, and auto marker/footprint choices can differ —
retry the full call for a set_ref when the deck must be consistent. In
query mode a re-run re-resolves the set and positions may not line up at
all, so retry for a set_ref or narrow the query. Mind the two position bases: images[].index and
spotlight_indices count DRAWABLE features only, so a skipped input (a
point with no marker config, say) shifts them relative to your own
features[] positions — a note says so whenever anything was skipped. A
point DRAWN as a marker is not skipped and does count. A response with fewer images than
features is NOT always a shortfall — spotlight_indices selects a subset
deliberately, and that response carries no such note. Per-feature `spotlight` flags
are ignored in this mode, and the per-feature list is suppressed by default
because every image already names the feature it shows. Filenames are
stable: they are derived from the whole set, so re-rendering one index
alone produces the same filename it had in the full batch. An image whose
own label is not on the card — dropped for overlap, no position that keeps
it inside the frame, a marker that could not be placed (the name goes with
the symbol), or the feature having no name to draw — reports
label_shown=false, and the note counts those four reasons separately: the
first, second and fourth in one breakdown of cards that show their shape
without its name, and the marker case in a note of its own, because there
the SUBJECT is missing rather than its caption (images[].marker_shown=false
marks exactly those). Caption a card from images[].label, which carries the
same label-or-name text the picture and the filename were derived from —
but only caption it as a picture of that feature when marker_shown is not
false.

Markers (for "where are all the X" maps). Pass `marker_default` (any
settings, even {}) to turn on marker rendering for the call, or `marker`
on an individual feature to override it there; unset fields inherit from
the default. A marker is a fixed-size symbol in CANVAS pixels — shape
(circle, square, triangle, diamond, star, pin, cross), size (4-128px),
fill/stroke (hex only), stroke_width, opacity — so it stays legible at any
extent. Size is per-feature, so passing your own number per feature builds
a proportional-symbol map. The default mode is auto: a footprint whose
larger drawn INK dimension is below `below_px` (default 4px, measured
after projection and canvas fit, and counting the stroke the shape is
drawn with — a hairline path under a fat stroke is as visible as its ink)
is replaced by a marker at its representative
point, and anything bigger draws as its true geometry — so a state
boundary stays a boundary while the 1 km² quarries inside it become dots.
mode='marker' forces symbols, mode='footprint' opts a feature out. Only
VISIBLE ink is measured: a fully transparent stroke, however wide, adds
nothing to the extent. A marker that would draw nothing at all —
opacity=0, or a zero-alpha fill AND stroke — is an ERROR naming the field
rather than a silent blank: it would still hold a drawable position, block
the collision space and canvas room a visible symbol could have used, and
report marker_shown=true — while painting nothing on any card of a deck. The same refusal applies to a footprint whose fill and stroke
are both fully transparent. Partial transparency is untouched. Both checks
run against every path a feature COULD take, not just the one this width
selected, so the verdict belongs to the request: an auto feature with an
invisible marker style is refused even when its footprint is what would
have drawn, and a set_ref therefore never stores a style that would fail on
a replay at another width. mode='footprint' is the opt-out and its marker
style is inert, so it is exempt — and so is a set_ref minted before this
check existed: rather than failing a handle you are already holding, such a
set replays WITHOUT the visibility rejection, draws nothing for that feature
and says so in the note. The exemption is from the CHECK only; rendering
always uses current semantics, so the picture can differ from what that
handle produced historically wherever the old output depended on a defect
this PR fixed — an invisible stroke taking up layout space, or being
repainted as opaque grey when dimmed.
Point/node features draw as markers when marker mode is on for them, and
when it is not they take the same skip PATH they took before markers
existed (both cases carry their own note). A point drawn as a marker is a DRAWABLE feature in every sense: it
takes a position in the drawable order that spotlight_indices and
images[].index address, gets a colour, is stored in the set_ref, and can be
spotlit — so turning markers on can renumber those positions relative to a
marker-free call of the same input. Points that share a longitude (or a
latitude) render normally: their symbols are sized in canvas pixels, so the
canvas reserves room for the symbols themselves rather than deriving a
sliver from a coordinate span of zero. That reserve is bounded by your
`width`, which stays a maximum: a symbol too large for the canvas you asked
for is hidden and reported, never accommodated by a wider image. Markers obey the same placement
invariant labels do: every marker drawn is fully inside the frame, one that
would overlap an already-placed marker is nudged aside (up to 48 candidate
positions), and one with nowhere to go — no free position, a symbol larger
than the canvas (width is a maximum: the canvas is never enlarged to fit a
symbol), or no
representative point at all — is not drawn and reports marker_shown=false
with a note saying which of the three — never clipped, never stacked. One feature gets
exactly ONE marker, at its own representative point, so a multi-part
geometry (MultiPoint, MultiPolygon, MultiLineString) drawn as a marker has
parts with no symbol of their own; the response says so, and sending each
part as its own `features[]` entry marks them all. A marker
feature's label is anchored under its symbol, nudge included. Markers dim
with the spotlight like any other feature — dimming recolours, it never
creates ink, so a fill or stroke you made fully transparent stays
transparent on the cards where the feature is dimmed — and, like the
per-feature
colours — the resolved marker is baked into the set_ref at mint, so
marker_default cannot be passed with set_ref; re-render from features or
query to restyle. One limit worth knowing: `query` mode matches ways and
relations only, so it never returns node features at all — marker mode
makes a query's small polygons visible, but to put symbols on nodes pass
them in `features` mode (osm_type="node"), e.g. from query_features.

Geometry handling: polygons draw filled + stroked (holes preserved),
lines draw as unfilled stroked paths, and points draw as markers when the
request asked for markers — without marker config they are still skipped
(a point has no outline to draw, and unasked-for symbols are not invented)
with a note saying markers would draw them. Relations that are boundaries
or multipolygons are assembled strictly — an incomplete member chain fails
the whole render rather than uploading a plausible-but-wrong map — while
route/waterway relations render as lines. Nested relation members are
assembled too (common outside the US: Swiss cantons carry shared lake
boundaries, Tokyo wards reclaimed-land fragments), fetched level by level
within the same Overpass budget, with cycles and deep nesting bounded; a
sub-relation whose members cannot be resolved, whose inner/outer role is
contradicted by its own members, or which is not itself an area relation
(a route, a waterway, or one carrying no type tag — its ways are not
boundary segments) still fails the render rather than guessing, and that
error names every affected object at once so one follow-up pass clears
them all. output selects svg, png, or
both; width sets the maximum pixel width — output is normally exactly that
wide, but height is derived proportionally and capped at 4096px, so a very
tall geometry renders narrower than requested (the result reports actual
width/height, never below 64px wide); projection is mercator (default) or
equal_area for continent scale; labels is tri-state
(unset auto-shows at <=100 features). Label SIZE is label_scale, not width:
labels are drawn at 13px * label_scale (0.5–4.0, default 1.0), rounded to a
whole pixel and floored at 11px, and that base is in canvas pixels
independent of width — so rendering wider does not make the text
relatively bigger, it makes it relatively smaller once the image is fitted
to a column. Raise label_scale (1.5–2.5 for a ~700px column) to get legible
text at any width; scales that compute below the 11px floor are raised to
it, and a note reports the raised size in exactly that case (no note names
the size when the scale is honoured as asked). Every
label that is drawn is fully
visible, in both SVG and PNG: labels are measured at the scaled size, then
clamped inward off their anchor so the text box clears the canvas edge,
then deconflicted at that same scaled size, and whatever still
cannot be placed is omitted with label_shown=false and a note — a label is
never clipped by the frame. Bigger text therefore means fewer labels, by
design. SVG text is pinned to the scaled measured width
(textLength), so the guarantee does not depend on which font the viewer
substitutes. label_scale is per-call like width: it is not stored in a
set_ref, and in spotlight="each" every image of the batch shares the one
scaled sizing and the one set of surviving labels. Hosted URL lifetime depends on the
delivery path (up to about 24 hours via CDN, or about 1 hour for the
presigned-S3 fallback); read expires_at for the authoritative expiry. Any
aoi_ref inputs expire after about 48 hours.

The result returns the resolved colour per feature so you can narrate the
map without fetching it. Output always carries OpenStreetMap ODbL
attribution that you must display wherever the map is shown.
Parameter Type Required Description
features array<OutlineFeatureSpec> | null No Explicit list of shapes to draw (max 100). Provide exactly one of features, query, or set_ref.
query OutlineQuerySpec | null No Resolve-and-render mode. Provide exactly one of features, query, or set_ref.
set_ref string | null No Replay a feature set this tool already resolved: the `set_ref` from an earlier render_outline response ('set_' + 12 hex). It stands in for the whole features/query input, so you never resend the list — no OpenStreetMap round-trip, no geometry through the conversation. Per-call options still apply and are NOT baked into the set: output, width, projection, labels, label_scale, attribution_caption, simplify_tolerance_m (coarsening only), and spotlight_indices. Per-feature colours/labels/markers are fixed at mint time — re-render from features to restyle, and note that `marker_default` is refused with set_ref for that reason. This is NOT an aoi_ref (which is a single geometry): a set_ref only goes here. Sets refs expire after about 48 hours; after that, re-render from features or query to mint a new one.
spotlight_indices array<integer> | null No With set_ref only: 0-based DRAWABLE positions into the stored set (the order of the `features` array in the response that minted it, which holds every feature that took a drawable POSITION — inputs skipped at mint time, a point with no marker config say, are absent and do not count, while a feature whose marker the placement pass could not fit KEEPS its position and reports marker_shown=false: positions are eligibility to be drawn, not proof of ink) to spotlight; every other feature renders dimmed. This REPLACES the set's stored spotlight flags rather than adding to them, so [7] spotlights exactly feature 7. An empty list [] means 'no spotlight at all' (a plain map); omitting the field keeps whatever was spotlit at mint time. Negative indices, duplicates and out-of-range positions are errors, not silently fixed. One card per call: {set_ref, spotlight_indices: [i], output: 'png'}. With spotlight='each' this instead selects WHICH features get their own image (omit it for all of them).
spotlight string | null No Batch mode. "each" returns ONE IMAGE PER FEATURE from a single call: feature i spotlit, every other feature dimmed, N images in `images` — a whole flashcard deck in one round-trip instead of N. "Spotlit" names the subject of a card, not a guarantee it is drawn: a marker that could not be placed is on no image at all, and its own card reports images[].marker_shown=false. The set is resolved, projected and fitted once, so every image shares the same canvas and framing and only the colouring differs. Each entry carries that feature's name/osm_type/osm_id, its png_url/svg_url and a `filename` derived server-side from that same feature's name, so a label can never drift onto the wrong picture. N is capped at 100 IMAGES per call: over that the call fails fast with the exact count and renders nothing. Works with features, query and set_ref; with set_ref it is the zero-resend path (one short call, N cards). With spotlight_indices it batches only those positions — which is also how a bigger set is rendered, one slice at a time, and how a batch that ran out of wall-clock is continued (that continuation needs the response's set_ref; without one the note names the unrendered features instead, in features mode by their position in the features[] list you sent — which is not images[].index whenever an input was skipped). Per-feature `spotlight` flags are ignored — every image spotlights exactly one feature. Leave unset for the normal single-image render.
marker_default OutlineMarkerSpec | null No Marker settings applied to EVERY feature of this call that does not override them per feature — the only way to mark a query's results, which have no per-feature slot. Setting it (even to {}) turns marker mode ON for the whole call: in the default mode='auto' that means footprints stay footprints while too-small-to-see shapes and point features become symbols. Leave it unset (and pass no per-feature `marker`) to keep the pre-marker RENDERING PATH: points skipped, every footprint drawn at true scale, no marker fields to read. That is a promise about the path, not about pixels — the visibility check, the ink-aware padding and the transparency-preserving dim palette apply to every request. Like the per-feature colours, the resolved marker is BAKED INTO a set_ref at mint time, so it cannot be passed together with set_ref — re-render from features/query to restyle.
output "svg" | "png" | "both" No Which artifact(s) to produce; SVG is the crisp default. Default: "svg"
width integer No Maximum output width in pixels (height is derived proportionally; max 4096). Normally the output is exactly this wide, but a very tall geometry that hits the 4096px height cap renders proportionally narrower (never below 64px — taller still is rejected). The result reports the actual width/height. Default: 1024
projection "mercator" | "equal_area" No mercator (default) or equal_area (Lambert azimuthal, for continent scale). Default: "mercator"
labels boolean | null No Tri-state: unset auto-shows labels at <=100 features and hides them above; true/false force. Labels use each feature's label or OSM name. Text size is set by label_scale, NOT by width. A label is drawn only if it fits entirely inside the canvas: it is first nudged inward off its feature's anchor so its text box clears the frame, then dropped if it still collides with an already-placed label or is too big for the canvas at any position. In marker mode a fourth case drops a label: a feature whose marker could not be placed keeps neither symbol nor name, so no text floats where nothing is drawn. Dropped labels report label_shown=false with a note; nothing is ever half-drawn, in SVG or PNG (SVG text is pinned to the measured width, so the viewer's own font cannot lay it out past the frame). PNG labels cover Latin (incl. umlauts/accents), Greek, and Cyrillic; CJK renders in SVG output but not PNG (tofu boxes) until a follow-up.
label_scale number No Label text size multiplier (0.5–4.0, default 1.0; out-of-range is an error). Labels are drawn at 13px * label_scale, rounded to a whole pixel and never below 11px (the floor is applied AFTER the multiplier and wins, so every scale below ~0.85 renders at the floor, and a note reports the raised size whenever the floor overrides the scale — on other scales no note names the size). This is the lever for legibility, because width is not: the base size is in canvas pixels and does not depend on width, so a wider render makes text relatively SMALLER once the image is fitted to a display column. For a map destined for a ~700px column, label_scale 1.5–2.5 is usually what 'readable' means. Bigger text means FEWER labels, by design: the scaled extents drive the edge clamp and the collision pass, so labels that no longer fit are dropped with label_shown=false and counted in the notes — never clipped or half-drawn, in SVG or PNG. Per-call like width and labels: it is not baked into a set_ref, so a replay can rescale the same set, and in spotlight='each' every image of the batch shares the one scaled sizing. Default: 1
simplify_tolerance_m number | null No Explicit Douglas-Peucker tolerance in metres, overriding the adaptive vertex budget. On a set_ref replay it can only COARSEN the stored geometry: a coarser value is applied on top of it (the result may be slightly coarser than a fresh render at that tolerance), while an equal or finer value leaves the stored geometry untouched and the response reports the stored tolerance, because detail already dropped cannot be recovered.
echo_features boolean | null No Whether the response repeats the per-feature list (name, osm id, colour, label_shown). It lists the DRAWABLE features in drawable order — skipped inputs (a point with no marker config, say) are absent, so entry i is not features[i] of your request when anything was skipped. Defaults to true when you pass features or query (that response is how you learn each feature's index for spotlight_indices), and to false when you pass set_ref (you already have the list) or spotlight='each' (every image already names its own feature, so echoing would report each one twice). When suppressed, `features` comes back empty and every count, the set_ref, the image URLs, dimensions, attribution and notes are unchanged — so a long render loop stays a fixed cost per call. It never touches `images`: in batch mode each image always carries its own feature's identity and filename. Pass true to get the full list anyway.
attribution_caption boolean No Burn a small ODbL caption into the PNG corner. The SVG always carries attribution in its metadata; you must display the attribution wherever the map is shown. Default: true
Parameter schema (JSON)
{
  "$defs": {
    "OutlineFeatureSpec": {
      "description": "One shape to draw. Provide exactly one geometry source.",
      "properties": {
        "aoi_ref": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Stored AOI handle (lookup_place/make_aoi/query_features); resolves server-side to full-fidelity geometry. Exactly one of aoi_ref, osm_type+osm_id, or geojson.",
          "title": "Aoi Ref"
        },
        "osm_type": {
          "anyOf": [
            {
              "enum": [
                "node",
                "way",
                "relation"
              ],
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "OSM object type, paired with osm_id.",
          "title": "Osm Type"
        },
        "osm_id": {
          "anyOf": [
            {
              "minimum": 1,
              "type": "integer"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "OSM object id, paired with osm_type.",
          "title": "Osm Id"
        },
        "geojson": {
          "anyOf": [
            {
              "additionalProperties": true,
              "type": "object"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Inline GeoJSON geometry (WGS84 lon/lat). Polygon/MultiPolygon and LineString/MultiLineString render as outlines. A Point/MultiPoint renders as a marker when this feature has marker config (its own `marker`, or the request's `marker_default`) and is otherwise skipped with a note, because an outline is all this tool draws without one. Other types are rejected. Capped at 2000 vertices.",
          "title": "Geojson"
        },
        "stroke": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Outline colour as a hex literal (#RGB, #RRGGBB, or #RRGGBBAA). No named colours or rgb(). Defaults to a palette colour.",
          "title": "Stroke"
        },
        "stroke_width": {
          "anyOf": [
            {
              "exclusiveMinimum": 0,
              "maximum": 64,
              "type": "number"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Stroke width in pixels (0-64]. Canvas padding grows by half the widest stroke so wide strokes are never cropped; a very wide stroke therefore needs a width comfortably above 2x(16 + stroke_width/2) pixels. Fractional widths are accepted and the SVG paints them, but the PNG has no sub-pixel pen: it draws any positive width at the nearest whole pixel, minimum 1. Framing, padding and the auto marker threshold measure the WIDER of the two, so a 0.9px stroke counts as the 1px the PNG shows and a 2.4px one counts as the 2.4 the SVG shows — the conservative direction, in which nothing is clipped in either format.",
          "title": "Stroke Width"
        },
        "fill": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Fill colour as a hex literal (#RGB, #RRGGBB, or #RRGGBBAA); polygons only. Defaults to a palette colour.",
          "title": "Fill"
        },
        "label": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Text label placed at the shape's representative point; falls back to the OSM name. Trimmed to 80 characters.",
          "title": "Label"
        },
        "marker": {
          "anyOf": [
            {
              "$ref": "#/$defs/OutlineMarkerSpec"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Draw this feature as a fixed-size symbol instead of (or, in auto mode, only when it is too small to see as) its true footprint. Unset fields inherit from the request's `marker_default`; omitting both leaves this feature on the plain footprint path it took before markers existed — no symbol, no marker reporting (points are skipped as they always were)."
        },
        "spotlight": {
          "default": false,
          "description": "When any feature sets spotlight=true, every feature NOT in the spotlit set renders dimmed. Multiple spotlights are allowed (union).",
          "title": "Spotlight",
          "type": "boolean"
        }
      },
      "title": "OutlineFeatureSpec",
      "type": "object",
      "additionalProperties": false
    },
    "OutlineMarkerSpec": {
      "description": "Draw a feature as a fixed-size symbol in CANVAS pixels (#1966).\n\nEvery field is optional and every unset field INHERITS: first from the\nrequest-level `marker_default`, then from the built-in defaults named\nbelow. That is what makes `marker_default` alone enough for a whole query\n(\"mark every quarry\") while a single feature can still override one field.\n\nPresence is what switches the behaviour on. A feature with no marker\nconfig, in a request with no `marker_default`, keeps the RENDERING PATH it\nhad before this option existed — footprint if it is a polygon or line,\nskipped if it is a point, and no marker reporting. That is a promise about\nthe path, not about pixels (#1968 review round 16): the visibility check,\nthe ink-aware padding and the transparency-preserving dim palette apply to\nevery request, marker config or not, so output can differ from an older\nbuild wherever the old output depended on one of the defects those fixed.\nSee render_outline's own paragraph on current semantics.",
      "properties": {
        "mode": {
          "anyOf": [
            {
              "enum": [
                "auto",
                "marker",
                "footprint"
              ],
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "auto (the default): draw the true footprint when its larger drawn INK dimension — the shape's extent PLUS its stroke width, so a hairline shape with a fat stroke counts as the ink it shows — is at least below_px canvas pixels, and a marker when it is smaller — so big shapes stay shapes and invisible ones become visible dots. marker: always a marker. footprint: always the true geometry, i.e. opt this feature out of marker rendering. A POINT feature has no footprint: it draws as a marker under auto and marker, and under footprint it is skipped like any point (with its own note saying so).",
          "title": "Mode"
        },
        "shape": {
          "anyOf": [
            {
              "enum": [
                "circle",
                "square",
                "triangle",
                "diamond",
                "star",
                "pin",
                "cross"
              ],
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Symbol shape (default circle). Every shape is drawn inside a size x size box, centred on the feature's representative point — except pin, which is drawn with its TIP on that point (its box sits above it), because that is how a pin is read. Either way the whole symbol is kept inside the canvas, and may be nudged off the exact point to stay there or to avoid overlapping another marker.",
          "title": "Shape"
        },
        "size": {
          "anyOf": [
            {
              "maximum": 128,
              "minimum": 4,
              "type": "number"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Symbol width AND height in canvas pixels (4.0-128.0, default 12.0); out of range is an error. Fixed in canvas pixels, so it does not shrink with the extent — that is the point of a marker. Per-feature, so passing a size computed from each feature's own value (area, output, population) builds a proportional-symbol map; the tool does no scaling of its own.",
          "title": "Size"
        },
        "fill": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Symbol fill as a hex literal (#RGB, #RRGGBB, #RRGGBBAA). No named colours or rgb(). Defaults to the feature's own fill, else its palette colour. A zero-alpha fill is fine on its own; a zero-alpha fill AND a zero-alpha stroke together are refused, since the symbol would draw nothing while still holding a position.",
          "title": "Fill"
        },
        "stroke": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Symbol outline colour as a hex literal. Defaults to the feature's own stroke, else #333333.",
          "title": "Stroke"
        },
        "stroke_width": {
          "anyOf": [
            {
              "exclusiveMinimum": 0,
              "maximum": 64,
              "type": "number"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Symbol outline width in pixels (0-64], default 1.0. It is part of the marker's drawn box, so it is included in the edge clamp and the overlap test rather than being allowed to spill — measured at whichever of the two formats paints it wider (a 0.4px ring reserves the 1px the PNG draws; a 2.4px ring reserves the 2.4 the SVG draws), unless the stroke is fully transparent, in which case it takes up no space in the frame check, the overlap test, the canvas reservation or the label offset, because it puts no ink on the image. It is still emitted at the width you asked for.",
          "title": "Stroke Width"
        },
        "opacity": {
          "anyOf": [
            {
              "maximum": 1,
              "minimum": 0,
              "type": "number"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "0-1 multiplier folded into the fill and stroke alpha (default 1.0). Multiplicative with any alpha already in those colours. A dimmed (non-spotlit) marker ignores it, exactly as a dimmed polygon ignores its own colours — with one exception: a component you made FULLY transparent stays transparent when dimmed, because the layout treated it as no ink and painting it grey would put ink where nothing was reserved. 0 is REFUSED with an error: a marker that draws nothing would still hold a drawable position, block the collision space and canvas room a visible marker could have used, and report marker_shown=true — while painting nothing on any card. Any value above 0 is fine (a value too small for the 8-bit alpha channel is drawn at the faintest alpha that channel can express, never dropped to nothing); to leave a feature out of marker rendering use mode='footprint'. The check runs whenever the feature COULD draw as a marker — mode 'auto' included, even at a width where its footprint is what draws — so a stored set never fails on a later replay.",
          "title": "Opacity"
        },
        "below_px": {
          "anyOf": [
            {
              "maximum": 512,
              "minimum": 0,
              "type": "number"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Auto-mode threshold in canvas pixels (default 4.0, max 512.0). Under mode='auto' a footprint is replaced by a marker when its larger drawn INK dimension is STRICTLY below this; at exactly this many pixels the footprint is drawn. INK means the path extent PLUS the stroke it is drawn with (strokes are centred on the path, so a 3px shape stroked at 64px shows ~67px and is NOT replaced) — and only VISIBLE ink counts: a fully transparent stroke adds nothing to the measurement, however wide it is, while a fractional one counts at whichever format paints it wider — the PNG's nearest whole pixel (minimum 1) or the SVG's exact width. Ignored under mode='marker'/'footprint' and for point features.",
          "title": "Below Px"
        }
      },
      "title": "OutlineMarkerSpec",
      "type": "object",
      "additionalProperties": false
    },
    "OutlineQuerySpec": {
      "description": "Resolve-and-render server-side: match OSM features, then draw them.",
      "properties": {
        "tags": {
          "anyOf": [
            {
              "items": {
                "$ref": "#/$defs/TagFilter"
              },
              "type": "array"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "AND-combined tag filters (single group). Provide tags or any_of.",
          "title": "Tags"
        },
        "any_of": {
          "anyOf": [
            {
              "items": {
                "items": {
                  "$ref": "#/$defs/TagFilter"
                },
                "type": "array"
              },
              "type": "array"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "OR-combined groups (each AND-combined). Provide tags or any_of.",
          "title": "Any Of"
        },
        "area_name": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Exact, case-sensitive OSM area name. One area selector required. Every same-named area is drawn (reported in the note); above 200 the name is refused in favour of area_osm_type + area_osm_id.",
          "title": "Area Name"
        },
        "area_osm_type": {
          "anyOf": [
            {
              "enum": [
                "way",
                "relation"
              ],
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Area boundary OSM type, paired with area_osm_id.",
          "title": "Area Osm Type"
        },
        "area_osm_id": {
          "anyOf": [
            {
              "minimum": 1,
              "type": "integer"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Area boundary OSM id.",
          "title": "Area Osm Id"
        },
        "aoi_ref": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Stored polygonal AOI handle to search inside.",
          "title": "Aoi Ref"
        },
        "exclude_private": {
          "default": false,
          "description": "Drop access=private/no features.",
          "title": "Exclude Private",
          "type": "boolean"
        },
        "max_features": {
          "default": 200,
          "description": "Cap on matched OSM objects (ways + relations), default 200, hard cap 1000. Over the cap fails fast with the exact match count — no partial render. This bounds object COUNT, not geometry weight: heavy boundaries (counties, states) can exhaust the render budget well below the cap, which returns an error naming what to reduce rather than a partial map.",
          "maximum": 1000,
          "minimum": 1,
          "title": "Max Features",
          "type": "integer"
        }
      },
      "title": "OutlineQuerySpec",
      "type": "object",
      "additionalProperties": false
    },
    "TagFilter": {
      "properties": {
        "key": {
          "description": "OSM tag key, e.g. 'leisure', 'amenity', 'sport'.",
          "minLength": 1,
          "title": "Key",
          "type": "string"
        },
        "value": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "OSM tag value, e.g. 'tennis'. Omit to match any feature that has the key. Common natural-language misnomers are auto-corrected (e.g. leisure=tennis_court becomes leisure=pitch + sport=tennis, amenity=gas_station becomes amenity=fuel) and reported in the response note; unknown values for well-known keys fail fast with suggestions.",
          "title": "Value"
        }
      },
      "required": [
        "key"
      ],
      "title": "TagFilter",
      "type": "object",
      "additionalProperties": false
    }
  },
  "properties": {
    "features": {
      "anyOf": [
        {
          "items": {
            "$ref": "#/$defs/OutlineFeatureSpec"
          },
          "maxItems": 100,
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Explicit list of shapes to draw (max 100). Provide exactly one of features, query, or set_ref.",
      "title": "Features"
    },
    "query": {
      "anyOf": [
        {
          "$ref": "#/$defs/OutlineQuerySpec"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Resolve-and-render mode. Provide exactly one of features, query, or set_ref."
    },
    "set_ref": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Replay a feature set this tool already resolved: the `set_ref` from an earlier render_outline response ('set_' + 12 hex). It stands in for the whole features/query input, so you never resend the list — no OpenStreetMap round-trip, no geometry through the conversation. Per-call options still apply and are NOT baked into the set: output, width, projection, labels, label_scale, attribution_caption, simplify_tolerance_m (coarsening only), and spotlight_indices. Per-feature colours/labels/markers are fixed at mint time — re-render from features to restyle, and note that `marker_default` is refused with set_ref for that reason. This is NOT an aoi_ref (which is a single geometry): a set_ref only goes here. Sets refs expire after about 48 hours; after that, re-render from features or query to mint a new one.",
      "title": "Set Ref"
    },
    "spotlight_indices": {
      "anyOf": [
        {
          "items": {
            "type": "integer"
          },
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "With set_ref only: 0-based DRAWABLE positions into the stored set (the order of the `features` array in the response that minted it, which holds every feature that took a drawable POSITION — inputs skipped at mint time, a point with no marker config say, are absent and do not count, while a feature whose marker the placement pass could not fit KEEPS its position and reports marker_shown=false: positions are eligibility to be drawn, not proof of ink) to spotlight; every other feature renders dimmed. This REPLACES the set's stored spotlight flags rather than adding to them, so [7] spotlights exactly feature 7. An empty list [] means 'no spotlight at all' (a plain map); omitting the field keeps whatever was spotlit at mint time. Negative indices, duplicates and out-of-range positions are errors, not silently fixed. One card per call: {set_ref, spotlight_indices: [i], output: 'png'}. With spotlight='each' this instead selects WHICH features get their own image (omit it for all of them).",
      "title": "Spotlight Indices"
    },
    "spotlight": {
      "anyOf": [
        {
          "const": "each",
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Batch mode. \"each\" returns ONE IMAGE PER FEATURE from a single call: feature i spotlit, every other feature dimmed, N images in `images` — a whole flashcard deck in one round-trip instead of N. \"Spotlit\" names the subject of a card, not a guarantee it is drawn: a marker that could not be placed is on no image at all, and its own card reports images[].marker_shown=false. The set is resolved, projected and fitted once, so every image shares the same canvas and framing and only the colouring differs. Each entry carries that feature's name/osm_type/osm_id, its png_url/svg_url and a `filename` derived server-side from that same feature's name, so a label can never drift onto the wrong picture. N is capped at 100 IMAGES per call: over that the call fails fast with the exact count and renders nothing. Works with features, query and set_ref; with set_ref it is the zero-resend path (one short call, N cards). With spotlight_indices it batches only those positions — which is also how a bigger set is rendered, one slice at a time, and how a batch that ran out of wall-clock is continued (that continuation needs the response's set_ref; without one the note names the unrendered features instead, in features mode by their position in the features[] list you sent — which is not images[].index whenever an input was skipped). Per-feature `spotlight` flags are ignored — every image spotlights exactly one feature. Leave unset for the normal single-image render.",
      "title": "Spotlight"
    },
    "marker_default": {
      "anyOf": [
        {
          "$ref": "#/$defs/OutlineMarkerSpec"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Marker settings applied to EVERY feature of this call that does not override them per feature — the only way to mark a query's results, which have no per-feature slot. Setting it (even to {}) turns marker mode ON for the whole call: in the default mode='auto' that means footprints stay footprints while too-small-to-see shapes and point features become symbols. Leave it unset (and pass no per-feature `marker`) to keep the pre-marker RENDERING PATH: points skipped, every footprint drawn at true scale, no marker fields to read. That is a promise about the path, not about pixels — the visibility check, the ink-aware padding and the transparency-preserving dim palette apply to every request.  Like the per-feature colours, the resolved marker is BAKED INTO a set_ref at mint time, so it cannot be passed together with set_ref — re-render from features/query to restyle."
    },
    "output": {
      "default": "svg",
      "description": "Which artifact(s) to produce; SVG is the crisp default.",
      "enum": [
        "svg",
        "png",
        "both"
      ],
      "title": "Output",
      "type": "string"
    },
    "width": {
      "default": 1024,
      "description": "Maximum output width in pixels (height is derived proportionally; max 4096). Normally the output is exactly this wide, but a very tall geometry that hits the 4096px height cap renders proportionally narrower (never below 64px — taller still is rejected). The result reports the actual width/height.",
      "maximum": 4096,
      "minimum": 64,
      "title": "Width",
      "type": "integer"
    },
    "projection": {
      "default": "mercator",
      "description": "mercator (default) or equal_area (Lambert azimuthal, for continent scale).",
      "enum": [
        "mercator",
        "equal_area"
      ],
      "title": "Projection",
      "type": "string"
    },
    "labels": {
      "anyOf": [
        {
          "type": "boolean"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Tri-state: unset auto-shows labels at <=100 features and hides them above; true/false force. Labels use each feature's label or OSM name. Text size is set by label_scale, NOT by width. A label is drawn only if it fits entirely inside the canvas: it is first nudged inward off its feature's anchor so its text box clears the frame, then dropped if it still collides with an already-placed label or is too big for the canvas at any position. In marker mode a fourth case drops a label: a feature whose marker could not be placed keeps neither symbol nor name, so no text floats where nothing is drawn. Dropped labels report label_shown=false with a note; nothing is ever half-drawn, in SVG or PNG (SVG text is pinned to the measured width, so the viewer's own font cannot lay it out past the frame). PNG labels cover Latin (incl. umlauts/accents), Greek, and Cyrillic; CJK renders in SVG output but not PNG (tofu boxes) until a follow-up.",
      "title": "Labels"
    },
    "label_scale": {
      "default": 1,
      "description": "Label text size multiplier (0.5–4.0, default 1.0; out-of-range is an error). Labels are drawn at 13px * label_scale, rounded to a whole pixel and never below 11px (the floor is applied AFTER the multiplier and wins, so every scale below ~0.85 renders at the floor, and a note reports the raised size whenever the floor overrides the scale — on other scales no note names the size). This is the lever for legibility, because width is not: the base size is in canvas pixels and does not depend on width, so a wider render makes text relatively SMALLER once the image is fitted to a display column. For a map destined for a ~700px column, label_scale 1.5–2.5 is usually what 'readable' means. Bigger text means FEWER labels, by design: the scaled extents drive the edge clamp and the collision pass, so labels that no longer fit are dropped with label_shown=false and counted in the notes — never clipped or half-drawn, in SVG or PNG. Per-call like width and labels: it is not baked into a set_ref, so a replay can rescale the same set, and in spotlight='each' every image of the batch shares the one scaled sizing.",
      "maximum": 4,
      "minimum": 0.5,
      "title": "Label Scale",
      "type": "number"
    },
    "simplify_tolerance_m": {
      "anyOf": [
        {
          "exclusiveMinimum": 0,
          "type": "number"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Explicit Douglas-Peucker tolerance in metres, overriding the adaptive vertex budget. On a set_ref replay it can only COARSEN the stored geometry: a coarser value is applied on top of it (the result may be slightly coarser than a fresh render at that tolerance), while an equal or finer value leaves the stored geometry untouched and the response reports the stored tolerance, because detail already dropped cannot be recovered.",
      "title": "Simplify Tolerance M"
    },
    "echo_features": {
      "anyOf": [
        {
          "type": "boolean"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Whether the response repeats the per-feature list (name, osm id, colour, label_shown). It lists the DRAWABLE features in drawable order — skipped inputs (a point with no marker config, say) are absent, so entry i is not features[i] of your request when anything was skipped. Defaults to true when you pass features or query (that response is how you learn each feature's index for spotlight_indices), and to false when you pass set_ref (you already have the list) or spotlight='each' (every image already names its own feature, so echoing would report each one twice). When suppressed, `features` comes back empty and every count, the set_ref, the image URLs, dimensions, attribution and notes are unchanged — so a long render loop stays a fixed cost per call. It never touches `images`: in batch mode each image always carries its own feature's identity and filename. Pass true to get the full list anyway.",
      "title": "Echo Features"
    },
    "attribution_caption": {
      "default": true,
      "description": "Burn a small ODbL caption into the PNG corner. The SVG always carries attribution in its metadata; you must display the attribution wherever the map is shown.",
      "title": "Attribution Caption",
      "type": "boolean"
    }
  },
  "title": "RenderOutlineParams",
  "type": "object",
  "additionalProperties": false
}
Result schema (JSON)
{
  "$defs": {
    "OutlineFeatureResult": {
      "properties": {
        "name": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Name"
        },
        "osm_type": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Osm Type"
        },
        "osm_id": {
          "anyOf": [
            {
              "type": "integer"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Osm Id"
        },
        "color": {
          "description": "Resolved colour assigned to this feature.",
          "title": "Color",
          "type": "string"
        },
        "label_shown": {
          "default": false,
          "description": "Whether a label was drawn for this feature. False means the name is absent from the image entirely, for one of four reasons — the feature has no label or name to draw; its label overlapped an already-placed one; it could not fit inside the canvas at any position; or (marker mode) its marker could not be placed, so the name was suppressed with the symbol rather than left floating where nothing is drawn (marker_shown=false says so). Never that it was drawn and clipped.",
          "title": "Label Shown",
          "type": "boolean"
        },
        "marker_shown": {
          "anyOf": [
            {
              "type": "boolean"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Marker rendering only (#1966). Null means this feature was drawn as its true footprint, so there is no marker to report — that is the value for every feature of a request with no marker config. True means its marker is on the image; false means nothing at all was drawn for this feature, for one of three reasons — the placement pass found no free position for it inside the frame; the symbol is larger than the canvas, which `width` bounds as a maximum and is never enlarged to fit a symbol; or the geometry yielded no representative point to draw it at. The note says which. A marker is never clipped or drawn on top of another marker.",
          "title": "Marker Shown"
        }
      },
      "required": [
        "color"
      ],
      "title": "OutlineFeatureResult",
      "type": "object"
    },
    "OutlineImageResult": {
      "description": "One image from a spotlight='each' batch: the feature it spotlights, the\nhosted artifact(s), and the filename derived from that same feature.",
      "properties": {
        "name": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "OSM name of the feature THIS image spotlights. Null for inline geometry with no name — caption from `label`, not from this.",
          "title": "Name"
        },
        "label": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "CAPTION THIS IMAGE WITH THIS. The feature's display text: its own label when you gave one, else its OSM name — the string the map draws when it draws one (see label_shown, and marker_shown for whether the feature is on the card at all) and the one `filename` is slugified from, so caption, picture and file agree. It is the feature's text either way, which is why it is safe to caption from. Null only for inline geometry with neither.",
          "title": "Label"
        },
        "osm_type": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Osm Type"
        },
        "osm_id": {
          "anyOf": [
            {
              "type": "integer"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Osm Id"
        },
        "png_url": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Hosted PNG URL, present when output is png or both.",
          "title": "Png Url"
        },
        "svg_url": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Hosted SVG URL, present when output is svg or both.",
          "title": "Svg Url"
        },
        "filename": {
          "description": "Suggested filename STEM (lowercase slug, no extension — append .png or .svg for the artifact you use). Derived here from this feature's label-or-name, the same text the map labels it with, so the caption and the picture cannot drift apart; unique within the response. Use it verbatim rather than naming files yourself.",
          "title": "Filename",
          "type": "string"
        },
        "index": {
          "description": "0-based DRAWABLE position: the feature's place in the rendered set (inputs that were skipped — a point with no marker config, say — are not counted; a point DRAWN as a marker is a drawable feature and IS counted), which is the basis spotlight_indices uses — pass this value there to re-render exactly this image, which requires the response's set_ref to be non-null AND the same per-call options (output, width, projection, labels, label_scale, simplify_tolerance_m, attribution_caption), since those are re-decided per call and drive which artifacts come back, label placement, the caption and the auto marker/footprint choice. It is NOT the position in the features[] list you sent whenever anything was skipped; the only field that reports original input positions is the no-set_ref continuation note, and that note labels itself as such. It addresses THIS set only: with no set_ref there is nothing to replay it against, and a re-resolved query can order its features differently.",
          "title": "Index",
          "type": "integer"
        },
        "label_shown": {
          "default": false,
          "description": "Whether THIS image carries the spotlit feature's own label. Label placement is one pass over one canvas, shared by every image of the batch, so a crowded map (or a label that cannot fit inside the frame) can drop the very name this card is about — false means the name is not on the card and the caption has to come from `label`. It says nothing about the SHAPE: read `marker_shown` for that. With marker_shown null or true the card shows the subject without its name; with marker_shown false the subject is not on the card at all, and its name went with it (#1968 review round 7). The note names the reasons separately (overlap, too large for the canvas, a marker that could not be placed, or no name to draw).",
          "title": "Label Shown",
          "type": "boolean"
        },
        "marker_shown": {
          "anyOf": [
            {
              "type": "boolean"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Whether the feature THIS image spotlights is actually drawn on it (#1968 review round 7). Null means the subject drew as its true footprint, so there is no marker to report — the value for every image of a request with no marker config. True means its marker is on the card. FALSE MEANS THE SUBJECT IS ABSENT: the placement pass could not fit its symbol (no free position, or a symbol larger than the canvas) or the geometry yielded no representative point, so this card shows the dimmed remainder and nothing of the feature it is named after — do not caption it as a picture of that feature. Mirrors features[].marker_shown for the same feature, and is reported per image because a batch suppresses the per-feature list by default. The response note says which reason applied.",
          "title": "Marker Shown"
        }
      },
      "required": [
        "filename",
        "index"
      ],
      "title": "OutlineImageResult",
      "type": "object"
    }
  },
  "properties": {
    "success": {
      "title": "Success",
      "type": "boolean"
    },
    "svg_url": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Hosted SVG URL. Lifetime depends on delivery path: up to 24h via CDN, or ~1h for the presigned-S3 fallback. Use expires_at for the authoritative expiry. Null in batch mode (spotlight='each') — the per-image URLs are in `images`.",
      "title": "Svg Url"
    },
    "png_url": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Hosted PNG URL. Lifetime depends on delivery path: up to 24h via CDN, or ~1h for the presigned-S3 fallback. Use expires_at for the authoritative expiry. Null in batch mode (spotlight='each') — the per-image URLs are in `images`.",
      "title": "Png Url"
    },
    "images": {
      "description": "Batch mode (spotlight='each') only: one entry per image, in set order, each spotlighting a different feature against the dimmed remainder. Every entry carries its own feature's identity and a server-derived `filename`, so nothing has to be paired up afterwards. \"Spotlighting\" is about which feature the card is ABOUT, not a promise that it is visible: a marker the placement pass could not fit leaves its own card showing only the dimmed remainder, and that entry reports marker_shown=false (read it before captioning).  Empty for a normal single-image render (see svg_url / png_url). It can be SHORTER than feature_count, in three different ways, so do not treat len(images) == feature_count as guaranteed: (1) you passed spotlight_indices, which deliberately selects which features get an image — the response is complete and carries no shortfall note; (2) the invocation ran out of wall-clock, or an image failed to render or upload, after at least one image succeeded — the images present are real and usable and the note says how many were rendered, why it stopped, and what is left; (3) nothing at all could be rendered, which is an error, not a short response. In case (2) a continuation call is spelled out ONLY when set_ref is non-null, and it matches the cards you already have only if you keep every option it prints — it names all of them (output, width, projection, labels, label_scale, simplify_tolerance_m, attribution_caption) because each is per-call: output decides which artifacts come back, attribution_caption whether the PNG is captioned, and the rest feed the canvas pixels that label placement and the auto marker/footprint threshold are decided in. When the set could not be stored the note instead names the unrendered features, adds `marker_default` to the options it prints (a resend re-styles from scratch, so the request-level default has to be repeated or the symbols are lost) and does NOT promise a pixel-exact resume: in features mode a resend of just those features returns the same features but re-fits the canvas to them, so framing, surviving labels and auto marker/footprint choices can differ (retry the full set for a set_ref if the deck must be consistent), and in query mode a re-run re-resolves a possibly different order, so the positions may not even address the same features. Two position bases exist and each is named where it is used: images[].index and spotlight_indices are DRAWABLE positions (skipped inputs — points with no marker config — are not counted, while a point drawn as a marker counts like any other drawn feature), while the no-set_ref features-mode note reports positions in the features[] list you sent, and says so. Entries are always in ascending drawable-set order.",
      "items": {
        "$ref": "#/$defs/OutlineImageResult"
      },
      "title": "Images",
      "type": "array"
    },
    "width": {
      "description": "Actual output width in pixels. May be less than the requested width when the 4096px height cap binds for a tall geometry (noted in `note`). Do NOT feed this value back as `width` to reproduce this render: `width` is a MAXIMUM that the fit-and-cap arithmetic turns into a scale, so passing the capped result as the next request's maximum yields a smaller canvas again. Pass the width you originally requested — which is exactly what a continuation note prints.",
      "title": "Width",
      "type": "integer"
    },
    "height": {
      "description": "Actual output height in pixels (derived; max 4096).",
      "title": "Height",
      "type": "integer"
    },
    "expires_at": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "RFC3339 UTC expiry of the hosted URLs: the EARLIEST expiry of every artifact in this response, so every returned URL is valid at least until this instant. That is across both artifacts with output=\"both\" (the SVG, uploaded first, expires first in the presigned fallback), and across every image of a batch. Null only when nothing was uploaded.",
      "title": "Expires At"
    },
    "feature_count": {
      "description": "Drawable features (polygons, lines, and points or small shapes drawn as markers). A marker the placement pass could not fit is still counted here — it holds its position in the drawable order — and reports marker_shown=false. In batch mode this is the size of the whole set: every image draws all of them EXCEPT any whose marker could not be placed (those are drawn on no image at all, and the card that spotlights one says so with marker_shown=false), one spotlit and the rest dimmed. So it is the number of images only when the batch covered the whole set and finished (see `images`).",
      "title": "Feature Count",
      "type": "integer"
    },
    "matched_count": {
      "anyOf": [
        {
          "type": "integer"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Query mode: raw matched OSM objects (ways + relations).",
      "title": "Matched Count"
    },
    "renderable_count": {
      "anyOf": [
        {
          "type": "integer"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Query mode: matched objects that produced a drawable outline.",
      "title": "Renderable Count"
    },
    "features": {
      "description": "One entry per DRAWABLE POSITION, in the set's order — the index of an entry here is the index to pass in spotlight_indices. A position-holding feature that ended up drawing nothing (its marker could not be placed) has an entry like any other, with marker_shown=false; skipped inputs have none. Empty when echo_features is false (the default for a set_ref replay and for spotlight='each', where `images` names the feature each image spotlights).",
      "items": {
        "$ref": "#/$defs/OutlineFeatureResult"
      },
      "title": "Features",
      "type": "array"
    },
    "set_ref": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Handle for this exact resolved feature set ('set_' + 12 hex). Pass it back as set_ref instead of the whole features/query input to re-render the same set — different width, projection, labels, output, or a different spotlight_indices — with no OpenStreetMap round-trip and no geometry in the conversation. It is NOT an aoi_ref and will not resolve in an aoi_ref slot. Sets refs expire after about 48 hours. Null when the set could not be stored (the note says why); replaying a set_ref echoes the same handle without rewriting the stored object.",
      "title": "Set Ref"
    },
    "effective_simplify_tolerance_m": {
      "anyOf": [
        {
          "type": "number"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Simplify tolerance actually applied; populated only when simplify_tolerance_m was explicitly set. In adaptive vertex-budget mode each feature gets its own per-feature allocation, so there is no single tolerance — this stays null and the budget is reported in note instead. On a set_ref replay it describes the geometry actually rendered, never merely what was requested: the stored tolerance when the request omits it, matches it, or asks for something finer (detail already dropped cannot come back), the requested tolerance when a coarsening pass really ran, and null for an untouched adaptive-budget snapshot.",
      "title": "Effective Simplify Tolerance M"
    },
    "attribution": {
      "title": "Attribution",
      "type": "string"
    },
    "note": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Semicolon-joined caveats about THIS response; null when there are none. Read it whenever `images` is shorter than expected: a batch that stopped early always says so here, with how many images were rendered, why it stopped, and either the continuation call to make (when set_ref is non-null — printed in full, with every per-call option to repeat) or the identities of the features left unrendered, together with what a resend can and cannot reproduce. Also carries skipped features, dropped labels, a failed set mint, and framing/simplification caveats.",
      "title": "Note"
    }
  },
  "required": [
    "success",
    "width",
    "height",
    "feature_count",
    "attribution"
  ],
  "title": "RenderOutlineResult",
  "type": "object"
}

# reverse_geocode read

Batch reverse geocode up to 25 coordinates to addresses via LocationIQ or Geoapify.

Full description
Reverse geocode coordinates to addresses with normalized components.

Runs up to 25 coordinate pairs in one call against the provider chosen
with `source` (the matching API key must be configured for the Geo
toolkit). `zoom` controls granularity from country (3) to building (18).
Address components (street, city, state, postcode, country, ...) are
normalized to the same keys regardless of provider. Calls are paced to
the provider's free-tier rate limit; if the provider still returns 429,
remaining coordinates are skipped and `rate_limited` is set. Results
include OpenStreetMap/provider attribution that must be shown when data
is displayed publicly.
Parameter Type Required Description
coordinates array<Coordinate> Yes Coordinates to reverse geocode. Up to 25 per call.
source "locationiq" | "geoapify" Yes Geocoding provider to use. Requires that provider's API key to be configured for the Geo toolkit.
zoom integer No Result granularity: 3 country, 5 state, 10 city, 14 suburb, 16 street, 18 building. LocationIQ honors all levels; Geoapify only supports country/state/city/street steps, so intermediate zooms map to the nearest coarser level (e.g. 14 returns city-level). Default: 18
Parameter schema (JSON)
{
  "$defs": {
    "Coordinate": {
      "properties": {
        "lat": {
          "description": "Latitude in WGS84 decimal degrees.",
          "maximum": 90,
          "minimum": -90,
          "title": "Lat",
          "type": "number"
        },
        "lon": {
          "description": "Longitude in WGS84 decimal degrees.",
          "maximum": 180,
          "minimum": -180,
          "title": "Lon",
          "type": "number"
        }
      },
      "required": [
        "lat",
        "lon"
      ],
      "title": "Coordinate",
      "type": "object",
      "additionalProperties": false
    }
  },
  "properties": {
    "coordinates": {
      "description": "Coordinates to reverse geocode. Up to 25 per call.",
      "items": {
        "$ref": "#/$defs/Coordinate"
      },
      "maxItems": 25,
      "minItems": 1,
      "title": "Coordinates",
      "type": "array"
    },
    "source": {
      "description": "Geocoding provider to use. Requires that provider's API key to be configured for the Geo toolkit.",
      "enum": [
        "locationiq",
        "geoapify"
      ],
      "title": "Source",
      "type": "string"
    },
    "zoom": {
      "default": 18,
      "description": "Result granularity: 3 country, 5 state, 10 city, 14 suburb, 16 street, 18 building. LocationIQ honors all levels; Geoapify only supports country/state/city/street steps, so intermediate zooms map to the nearest coarser level (e.g. 14 returns city-level).",
      "maximum": 18,
      "minimum": 3,
      "title": "Zoom",
      "type": "integer"
    }
  },
  "required": [
    "coordinates",
    "source"
  ],
  "title": "ReverseGeocodeParams",
  "type": "object",
  "additionalProperties": false
}
Result schema (JSON)
{
  "$defs": {
    "GeocodeMatch": {
      "properties": {
        "lat": {
          "title": "Lat",
          "type": "number"
        },
        "lon": {
          "title": "Lon",
          "type": "number"
        },
        "display_name": {
          "title": "Display Name",
          "type": "string"
        },
        "bbox": {
          "anyOf": [
            {
              "items": {
                "type": "number"
              },
              "type": "array"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "[west, south, east, north] in WGS84 degrees.",
          "title": "Bbox"
        },
        "osm_type": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "OSM object type: node, way, or relation.",
          "title": "Osm Type"
        },
        "osm_id": {
          "anyOf": [
            {
              "type": "integer"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Osm Id"
        },
        "category": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "OSM class, e.g. 'place', 'amenity', 'boundary'.",
          "title": "Category"
        },
        "type": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "OSM type within the class, e.g. 'city', 'restaurant'.",
          "title": "Type"
        },
        "importance": {
          "anyOf": [
            {
              "type": "number"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Relative importance score when the provider supplies one.",
          "title": "Importance"
        },
        "address": {
          "additionalProperties": {
            "type": "string"
          },
          "description": "Normalized address components.",
          "title": "Address",
          "type": "object"
        },
        "geojson": {
          "anyOf": [
            {
              "additionalProperties": true,
              "type": "object"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Boundary geometry as GeoJSON when include_geojson is set and available.",
          "title": "Geojson"
        }
      },
      "required": [
        "lat",
        "lon",
        "display_name"
      ],
      "title": "GeocodeMatch",
      "type": "object"
    },
    "ReverseGeocodeItemResult": {
      "properties": {
        "lat": {
          "title": "Lat",
          "type": "number"
        },
        "lon": {
          "title": "Lon",
          "type": "number"
        },
        "match": {
          "anyOf": [
            {
              "$ref": "#/$defs/GeocodeMatch"
            },
            {
              "type": "null"
            }
          ],
          "default": null
        },
        "error": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Error"
        }
      },
      "required": [
        "lat",
        "lon"
      ],
      "title": "ReverseGeocodeItemResult",
      "type": "object"
    }
  },
  "properties": {
    "success": {
      "title": "Success",
      "type": "boolean"
    },
    "source": {
      "title": "Source",
      "type": "string"
    },
    "attribution": {
      "title": "Attribution",
      "type": "string"
    },
    "rate_limited": {
      "default": false,
      "description": "True when the provider rate limit was hit and remaining coordinates were skipped.",
      "title": "Rate Limited",
      "type": "boolean"
    },
    "results": {
      "items": {
        "$ref": "#/$defs/ReverseGeocodeItemResult"
      },
      "title": "Results",
      "type": "array"
    }
  },
  "required": [
    "success",
    "source",
    "attribution",
    "results"
  ],
  "title": "ReverseGeocodeResult",
  "type": "object"
}

# route read

Route between 2-10 waypoints on the real road/path network (walk, drive, bike) via LocationIQ or Geoapify: routed distance and travel time, not straight-line.

Full description
Route between waypoints on the real road/path network.

Returns routed travel distance and estimated duration for walk, drive,
or bike between 2-10 waypoints visited in order — unlike measure's
distance op, which is straight-line. Uses the provider chosen with
`source` (the matching API key must be configured for the Geo toolkit):
LocationIQ Directions or Geoapify Routing, normalized to one result
shape. Set include_geometry for the route line as GeoJSON (e.g. to map
the walk). Results include provider attribution that must be shown when
data is displayed publicly.
Parameter Type Required Description
waypoints array<Coordinate> Yes Waypoints routed in order (2-10): start, optional vias, end.
mode "walk" | "drive" | "bike" No Travel mode: walk, drive, or bike. Default: "walk"
source "locationiq" | "geoapify" Yes Routing provider to use. Requires that provider's API key to be configured for the Geo toolkit.
include_geometry boolean No Include the route line as GeoJSON. Default: false
Parameter schema (JSON)
{
  "$defs": {
    "Coordinate": {
      "properties": {
        "lat": {
          "description": "Latitude in WGS84 decimal degrees.",
          "maximum": 90,
          "minimum": -90,
          "title": "Lat",
          "type": "number"
        },
        "lon": {
          "description": "Longitude in WGS84 decimal degrees.",
          "maximum": 180,
          "minimum": -180,
          "title": "Lon",
          "type": "number"
        }
      },
      "required": [
        "lat",
        "lon"
      ],
      "title": "Coordinate",
      "type": "object",
      "additionalProperties": false
    }
  },
  "properties": {
    "waypoints": {
      "description": "Waypoints routed in order (2-10): start, optional vias, end.",
      "items": {
        "$ref": "#/$defs/Coordinate"
      },
      "maxItems": 10,
      "minItems": 2,
      "title": "Waypoints",
      "type": "array"
    },
    "mode": {
      "default": "walk",
      "description": "Travel mode: walk, drive, or bike.",
      "enum": [
        "walk",
        "drive",
        "bike"
      ],
      "title": "Mode",
      "type": "string"
    },
    "source": {
      "description": "Routing provider to use. Requires that provider's API key to be configured for the Geo toolkit.",
      "enum": [
        "locationiq",
        "geoapify"
      ],
      "title": "Source",
      "type": "string"
    },
    "include_geometry": {
      "default": false,
      "description": "Include the route line as GeoJSON.",
      "title": "Include Geometry",
      "type": "boolean"
    }
  },
  "required": [
    "waypoints",
    "source"
  ],
  "title": "RouteParams",
  "type": "object",
  "additionalProperties": false
}
Result schema (JSON)
{
  "properties": {
    "success": {
      "title": "Success",
      "type": "boolean"
    },
    "source": {
      "title": "Source",
      "type": "string"
    },
    "attribution": {
      "title": "Attribution",
      "type": "string"
    },
    "mode": {
      "title": "Mode",
      "type": "string"
    },
    "distance_m": {
      "description": "Routed travel distance in meters (not straight-line).",
      "title": "Distance M",
      "type": "number"
    },
    "distance_km": {
      "title": "Distance Km",
      "type": "number"
    },
    "duration_min": {
      "description": "Estimated travel time in minutes.",
      "title": "Duration Min",
      "type": "number"
    },
    "geojson": {
      "anyOf": [
        {
          "additionalProperties": true,
          "type": "object"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Route line as GeoJSON when include_geometry is set.",
      "title": "Geojson"
    }
  },
  "required": [
    "success",
    "source",
    "attribution",
    "mode",
    "distance_m",
    "distance_km",
    "duration_min"
  ],
  "title": "RouteResult",
  "type": "object"
}