{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/thinair-co/json-schema/thinair-co-map-match-response-schema.json",
  "title": "MapMatchResponse",
  "description": "Matched-route result. `geojson` carries the snapped LineString for small traces; longer traces return `geometry.encoded` (polyline6 — decode with @mapbox/polyline at precision 6). `match_status` and `coverage` report how much of the trace the matcher actually placed on the road network — `matched` alone only says a route was returned. Road names/classes and distances are surfaced; internal road-graph identifiers are never exposed.",
  "x-generated": "2026-10-30",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/geo-thinair-co-openapi.json#/components/schemas/MapMatchResponse",
  "type": "object",
  "properties": {
    "matched": {
      "type": "boolean",
      "description": "True when a route was returned for the trace. It does NOT mean the whole trace matched — use `match_status`."
    },
    "distance_km": {
      "type": "number"
    },
    "duration_seconds": {
      "type": "number"
    },
    "confidence": {
      "type": "number",
      "description": "0–1: fraction of input GPS points within the snap tolerance of the matched route. A distance heuristic: it ignores point order and which parallel roadway was matched. For the matcher's own per-point result use `match_status` / `coverage`."
    },
    "road_names": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "legs": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": {}
      }
    },
    "costing": {
      "type": "string"
    },
    "geojson": {
      "type": "object",
      "additionalProperties": {}
    },
    "geometry": {
      "type": "object",
      "additionalProperties": {}
    },
    "meta": {
      "type": "object",
      "properties": {
        "upstream_ms": {
          "type": "number"
        },
        "point_count": {
          "type": "integer"
        },
        "breakage_distance_m": {
          "type": "integer",
          "description": "Echoes the breakage_distance_m the caller requested, for confirmation — present only when the caller set it. This is the value SENT to the matcher, not a claim about what it did with it."
        }
      },
      "required": [
        "upstream_ms",
        "point_count"
      ],
      "additionalProperties": {}
    },
    "match_status": {
      "type": [
        "string",
        "null"
      ],
      "enum": [
        "full",
        "partial",
        "none",
        null
      ],
      "description": "Whole-trace match status from the matcher's own per-point results. `full`: every input point was matched or interpolated onto the road network AND the match is one continuous section. `none`: no input point was matched or interpolated. `partial`: anything else — some points unmatched, or the match broke into more than one section. null when the per-point check could not be completed; `coverage.unavailable_reason` says why. Gate on this field, not on `matched`."
    },
    "coverage": {
      "type": "object",
      "properties": {
        "point_count": {
          "type": "integer",
          "description": "Number of input GPS points"
        },
        "matched_count": {
          "type": "integer",
          "description": "Input points the matcher placed on the road network directly. null when unavailable."
        },
        "interpolated_count": {
          "type": "integer",
          "description": "Input points the matcher placed on the road network by interpolating between matched neighbours; counted as covered. null when unavailable."
        },
        "unmatched_count": {
          "type": "integer",
          "description": "Input points the matcher could not place on the road network. null when unavailable."
        },
        "ratio": {
          "type": [
            "number",
            "null"
          ],
          "description": "0–1: (matched_count + interpolated_count) / point_count, rounded down to 3 decimals so a partial trace never reads 1. null when unavailable."
        },
        "unmatched_ranges": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": "object",
            "properties": {
              "start_index": {
                "type": "integer",
                "description": "0-based index of the first unmatched input point in this run"
              },
              "end_index": {
                "type": "integer",
                "description": "0-based index of the last unmatched input point in this run (inclusive)"
              },
              "point_count": {
                "type": "integer",
                "description": "Number of input points in this run"
              }
            },
            "required": [
              "start_index",
              "end_index",
              "point_count"
            ],
            "additionalProperties": {}
          },
          "description": "Contiguous runs of unmatched input points, as 0-based inclusive index ranges into `shape`, in trace order. Empty when every point was covered; null when unavailable."
        },
        "route_sections": {
          "type": "integer",
          "description": "Number of disjoint sections the match broke into (1 = continuous). When greater than 1, the returned geometry, distance and duration cover only the FIRST section — split the trace at the break and match the parts separately."
        },
        "unavailable_reason": {
          "type": [
            "string",
            "null"
          ],
          "enum": [
            "upstream_error",
            "timeout",
            "no_point_status",
            "point_count_mismatch",
            "unknown_point_type",
            null
          ],
          "description": "Why the per-point counts and `match_status` are null: `upstream_error` (the per-point check failed), `timeout` (it did not answer in time), `no_point_status` (it answered without per-point results), `point_count_mismatch` (its results did not line up one-to-one with the input points), `unknown_point_type` (it reported a status this API does not recognise). null when coverage is available."
        }
      },
      "required": [
        "point_count",
        "matched_count",
        "interpolated_count",
        "unmatched_count",
        "ratio",
        "unmatched_ranges",
        "route_sections",
        "unavailable_reason"
      ],
      "additionalProperties": {},
      "description": "Per-point coverage of the trace from the matcher's own per-point status — independent of the distance-based `confidence`. Every key is always present."
    }
  },
  "required": [
    "matched",
    "distance_km",
    "duration_seconds",
    "confidence",
    "road_names",
    "legs",
    "costing",
    "meta",
    "match_status",
    "coverage"
  ],
  "additionalProperties": {}
}