{
  "$ref": "#/definitions/PollOutput",
  "definitions": {
    "PollOutput": {
      "type": "object",
      "properties": {
        "output_type": {
          "type": "string",
          "minLength": 1,
          "description": "Type of output. Canonical values: \"core\", \"potential\", \"model\", \"participation\". Other values allowed.\n\nNote: \"model\" here means the polling agency's own weighted/adjusted figures — not anything produced by the aggregation pipeline. Poll-of-polls aggregates and Monte Carlo outputs belong in EstimateSnapshot."
        },
        "tags": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Descriptive tags for filtering and grouping. Examples: [\"parties\", \"main\"], [\"candidates\", \"first-round\"]"
        },
        "sub_population": {
          "type": "string",
          "description": "Sub-population this output covers, if narrower than the poll-level population. Example: \"Definite voters\""
        },
        "margin_of_error": {
          "type": "string",
          "description": "Margin of error for this output. Free string — agencies report in different formats. Examples: \"±3%\", \"2.8 pp\""
        },
        "lower_cut_percent": {
          "type": "number",
          "minimum": 0,
          "maximum": 100,
          "description": "Choices below this percent threshold are not individually reported by the agency. Example: 2.0 means parties under 2% are suppressed."
        },
        "under_lower_cut": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Named choices that were included in fieldwork but fell below lower_cut_percent and are not listed in results."
        },
        "results": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "choice_id": {
                "type": "string",
                "minLength": 1,
                "description": "ID of the choice (party, coalition) or candidate. References Choice.id or Candidate.id."
              },
              "value_percent": {
                "type": "number",
                "minimum": 0,
                "maximum": 100,
                "description": "Stated voting intention as a percentage of valid responses (0–100). Raw reported figure — not adjusted or modelled by the aggregation pipeline."
              }
            },
            "required": [
              "choice_id",
              "value_percent"
            ],
            "additionalProperties": false,
            "description": "A single choice's result within one poll output.\n\nLayer: source / ingestion (raw agency-reported data)\n\nThese are values exactly as the polling agency published them. A missing entry means the choice was not reported (possibly suppressed by lower_cut_percent). Derived/normalised values produced by the compute layer are stored in DerivedResult instead."
          },
          "description": "Per-choice voting intention figures for this output. May be empty when the output records only aggregate figures (e.g. a participation output with only undecided_percent)."
        },
        "others_percent": {
          "type": "number",
          "minimum": 0,
          "maximum": 100,
          "description": "Aggregate percentage for parties/candidates not individually listed in results (e.g. combined \"other parties\" figure reported by the agency)."
        },
        "undecided_percent": {
          "type": "number",
          "minimum": 0,
          "maximum": 100,
          "description": "Percentage of respondents who are undecided or answered \"don't know\"."
        },
        "abstention_percent": {
          "type": "number",
          "minimum": 0,
          "maximum": 100,
          "description": "Percentage of respondents who plan to abstain / will not vote."
        },
        "blank_percent": {
          "type": "number",
          "minimum": 0,
          "maximum": 100,
          "description": "Percentage of respondents who plan to cast a blank or null ballot."
        },
        "extras": {
          "type": "object",
          "additionalProperties": {},
          "description": "Extension point for additional fields."
        }
      },
      "required": [
        "output_type",
        "results"
      ],
      "additionalProperties": false,
      "description": "One \"view\" of results within a poll.\n\nLayer: source / ingestion (raw agency-reported data)\n\nA single poll may publish multiple outputs, e.g.: \"core\" (definite voters), \"potential\" (might vote), \"model\" (agency's own weighted figures), \"participation\" (turnout figures).\n\nIMPORTANT — scope boundary: PollOutput represents data exactly as the polling agency publishes it. output_type=\"model\" means the agency's own internal model, not the aggregation pipeline's model. Poll-of-polls aggregates, Monte Carlo simulations, and other computed estimates are stored in EstimateSnapshot — a completely separate schema that is never nested inside Poll."
    }
  },
  "$schema": "http://json-schema.org/draft-07/schema#"
}