{
  "$ref": "#/definitions/Poll",
  "definitions": {
    "Poll": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "minLength": 1,
          "description": "Unique stable identifier, e.g. \"kantar-cz-2025-02-01\"."
        },
        "region": {
          "anyOf": [
            {
              "type": "string",
              "minLength": 1
            },
            {
              "type": "array",
              "items": {
                "type": "string",
                "minLength": 1
              }
            }
          ],
          "description": "Region(s) this poll covers. ISO 3166-1 alpha-2 (country) or ISO 3166-2 (sub-national) codes."
        },
        "type": {
          "type": "string",
          "description": "Type of election being measured. Canonical values: \"parliamentary\", \"presidential\", \"municipal\", \"regional\", \"european\", \"referendum\"."
        },
        "pollster": {
          "type": "string",
          "description": "ID or name of the polling agency. Should reference Pollster.id when a pollsters.json reference file is maintained."
        },
        "sponsors": {
          "anyOf": [
            {
              "type": "string",
              "minLength": 1
            },
            {
              "type": "array",
              "items": {
                "type": "string",
                "minLength": 1
              }
            }
          ],
          "description": "Organisation(s) that commissioned the poll. String for single sponsor; array for multiple."
        },
        "url": {
          "anyOf": [
            {
              "type": "string",
              "format": "uri"
            },
            {
              "type": "array",
              "items": {
                "type": "string",
                "format": "uri"
              }
            }
          ],
          "description": "URL(s) of the published source(s) for these results."
        },
        "published_at": {
          "type": "string",
          "description": "Date (or datetime) results were publicly released (ISO 8601). Use the earliest known publication date."
        },
        "start_date": {
          "type": "string",
          "format": "date",
          "description": "First day of fieldwork (ISO 8601 date)."
        },
        "end_date": {
          "type": "string",
          "format": "date",
          "description": "Last day of fieldwork (ISO 8601 date)."
        },
        "central_date": {
          "type": "string",
          "description": "Representative date for time-series placement (ISO 8601). Typically the fieldwork midpoint."
        },
        "sample_size": {
          "anyOf": [
            {
              "type": "integer",
              "exclusiveMinimum": 0
            },
            {
              "type": "null"
            }
          ],
          "description": "Number of respondents interviewed. Null if not published."
        },
        "population": {
          "type": "string",
          "description": "Population the poll is intended to represent. Example: \"18+\", \"registered voters\", \"likely voters\""
        },
        "method": {
          "type": "string",
          "description": "How the poll was conducted. Example: \"100% CATI\", \"CAWI\", \"CAPI/CAWI mixed\""
        },
        "margin_of_error": {
          "type": "string",
          "description": "Overall margin of error. Free string. Examples: \"±3%\", \"2.8 pp\""
        },
        "outputs": {
          "type": "array",
          "items": {
            "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."
          },
          "minItems": 1,
          "description": "Poll outputs — one or more views of the results. At minimum one output with type \"core\" or \"model\" is expected. output_type=\"model\" refers to the agency's own model, not the aggregation pipeline's model (see EstimateSnapshot for that)."
        },
        "extras": {
          "type": "object",
          "additionalProperties": {},
          "description": "Extension point for additional fields."
        }
      },
      "required": [
        "id",
        "region",
        "outputs"
      ],
      "additionalProperties": false,
      "description": "A single published opinion poll.\n\nLayer: source / ingestion (raw agency-reported data)\n\nContains data exactly as reported by the polling agency — nothing here is computed or adjusted by the aggregation pipeline.\n\nData flow: Poll (source) → ScenarioSnapshot (compute) → EstimateSnapshot (aggregate).\n\nNote: output_type=\"model\" in a PollOutput means the agency's own weighted/adjusted figures. The aggregation pipeline's model outputs belong in EstimateSnapshot."
    }
  },
  "$schema": "http://json-schema.org/draft-07/schema#"
}