openapi: 3.1.0
info:
  title: Popcast Signal API
  version: 0.1.0
  description: |
    Block group social signal layer. Returns rolling word clouds, sentiment
    splits, and topic clusters keyed to US Census block group FIPS codes.

    Signal is aggregated from geo-tagged public posts (Reddit, Yelp, Google
    Places) via NLP topic extraction and sentiment classification. Refreshed
    on a daily cadence with 7, 14, and 30-day rolling windows.

    **Signal type distinction**
    - `at-place`: posts originating within the block group boundary.
      Represents mindstate of people physically present — the audience
      seeing OOH inventory in that geography.
    - `residential`: posts from users whose home location is inferred to
      be the block group. Represents mindstate of the resident audience.

    Consumers must distinguish signal type. Viewcast viewshed enrichment
    uses `at-place`. Popcast audience profiling uses `residential`.

    **Data sources (planned)**
    - Reddit API (subreddit-to-block-group crosswalk)
    - Yelp Fusion API (place lat/long → point-in-polygon FIPS attribution)
    - Google Places reviews (place lat/long → FIPS attribution)

    All endpoints are roadmap status (TF-96).
  contact:
    name: Motionworks AI
    url: https://docs.mworks.com

servers:
  - url: https://api.mworks.com
    description: Production

tags:
  - name: signal
    description: Block group social signal endpoints

paths:
  /v2/popcast/signal/{fips}:
    get:
      operationId: getBlockGroupSignal
      summary: Get current word cloud for a block group
      description: |
        Returns the current rolling word cloud, sentiment split, and top
        topics for a single census block group FIPS code.
      tags: [signal]
      x-motionworks-status: roadmap
      x-motionworks-roadmap-issue: TF-96
      x-credit-cost: 1
      parameters:
        - name: fips
          in: path
          required: true
          description: 12-digit US Census block group FIPS code
          schema:
            type: string
            pattern: '^\d{12}$'
            example: '130890030001'
        - name: window
          in: query
          description: Rolling time window in days
          schema:
            type: integer
            enum: [7, 14, 30]
            default: 30
        - name: signal_type
          in: query
          description: |
            Filter by signal attribution method.
            `at-place` = posts originating within the block group boundary.
            `residential` = posts from inferred home-location residents.
            `combined` = all sources for the FIPS, aggregated together.
          schema:
            type: string
            enum: [at-place, residential, combined]
            default: combined
      responses:
        '200':
          description: Block group signal returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SignalResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '404':
          $ref: '#/components/responses/NotFound'
        '501':
          $ref: '#/components/responses/RoadmapNotAvailable'

  /v2/popcast/signal/{fips}/history:
    get:
      operationId: getBlockGroupSignalHistory
      summary: Get time-series word cloud snapshots for a block group
      description: |
        Returns weekly or daily word cloud snapshots for trend analysis.
        Historical archive begins from ingestion pipeline go-live date.
      tags: [signal]
      x-motionworks-status: roadmap
      x-motionworks-roadmap-issue: TF-96
      x-credit-cost: 3
      parameters:
        - name: fips
          in: path
          required: true
          schema:
            type: string
            pattern: '^\d{12}$'
        - name: from
          in: query
          schema:
            type: string
            format: date
            example: '2025-01-01'
        - name: to
          in: query
          schema:
            type: string
            format: date
        - name: signal_type
          in: query
          schema:
            type: string
            enum: [at-place, residential, combined]
            default: combined
        - name: granularity
          in: query
          schema:
            type: string
            enum: [daily, weekly]
            default: weekly
      responses:
        '200':
          description: Historical snapshots returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SignalHistoryResponse'
        '501':
          $ref: '#/components/responses/RoadmapNotAvailable'

  /v2/popcast/signal/batch:
    post:
      operationId: batchGetBlockGroupSignal
      summary: Get word clouds for up to 100 block groups
      description: |
        Primary consumption interface for Viewcast viewshed enrichment
        (viewshed polygon decomposed to constituent FIPS codes) and
        digital itinerary creative relevance scoring.
      tags: [signal]
      x-motionworks-status: roadmap
      x-motionworks-roadmap-issue: TF-96
      x-credit-cost: 1
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SignalBatchRequest'
      responses:
        '200':
          description: Batch signal results returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SignalBatchResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '501':
          $ref: '#/components/responses/RoadmapNotAvailable'

components:
  schemas:

    WordCloudTerm:
      type: object
      required: [term, frequency, sentiment, source]
      properties:
        term:
          type: string
          description: Extracted topic term or phrase
          example: traffic
        frequency:
          type: number
          format: float
          minimum: 0
          maximum: 100
          description: Normalized frequency score within the window (0–100)
        sentiment:
          type: string
          enum: [positive, neutral, negative]
        source:
          type: string
          enum: [reddit, yelp, google_places, combined]
          description: Data source this term was primarily extracted from

    TopTopic:
      type: object
      required: [topic, post_count, sentiment]
      properties:
        topic:
          type: string
          example: commute_frustration
        post_count:
          type: integer
        sentiment:
          type: string
          enum: [positive, neutral, negative]

    Sentiment:
      type: object
      required: [positive_pct, neutral_pct, negative_pct]
      properties:
        positive_pct:
          type: number
          format: float
          minimum: 0
          maximum: 100
        neutral_pct:
          type: number
          format: float
          minimum: 0
          maximum: 100
        negative_pct:
          type: number
          format: float
          minimum: 0
          maximum: 100

    BlockGroupSignal:
      type: object
      required: [fips, signal_type, window_days, refreshed_at, post_count, sentiment, word_cloud, top_topics, data_quality]
      properties:
        fips:
          type: string
          pattern: '^\d{12}$'
          description: 12-digit US Census block group FIPS code
          example: '130890030001'
        signal_type:
          type: string
          enum: [at-place, residential, combined]
          description: Attribution method applied to the returned signal
        window_days:
          type: integer
          enum: [7, 14, 30]
        refreshed_at:
          type: string
          format: date-time
          description: UTC timestamp of last ingestion refresh
        post_count:
          type: integer
          description: Total posts ingested in the window for this block group
        sentiment:
          $ref: '#/components/schemas/Sentiment'
        word_cloud:
          type: array
          items:
            $ref: '#/components/schemas/WordCloudTerm'
          maxItems: 100
        top_topics:
          type: array
          items:
            $ref: '#/components/schemas/TopTopic'
          maxItems: 10
        data_quality:
          type: string
          enum: [high, medium, low]
          default: medium
          description: |
            Confidence band for this signal, parallel to Placecast's
            `is_focused`. Derived from post_count: >=50 = high,
            10-49 = medium, <10 = low. Consumers should down-weight
            'low' signals in aggregate analysis.

    SignalResponse:
      type: object
      required: [data, meta]
      properties:
        data:
          $ref: '#/components/schemas/BlockGroupSignal'
        meta:
          $ref: '#/components/schemas/ResponseMeta'

    SignalHistorySnapshot:
      type: object
      required: [period_start, period_end, fips, signal_type, post_count, sentiment, word_cloud]
      properties:
        period_start:
          type: string
          format: date
        period_end:
          type: string
          format: date
        fips:
          type: string
          pattern: '^\d{12}$'
        signal_type:
          type: string
          enum: [at-place, residential, combined]
        post_count:
          type: integer
        sentiment:
          $ref: '#/components/schemas/Sentiment'
        word_cloud:
          type: array
          items:
            $ref: '#/components/schemas/WordCloudTerm'
          maxItems: 50

    SignalHistoryResponse:
      type: object
      required: [data, pagination, meta]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/SignalHistorySnapshot'
        pagination:
          type: object
          properties:
            cursor:
              type: string
              nullable: true
            has_more:
              type: boolean
        meta:
          $ref: '#/components/schemas/ResponseMeta'

    SignalBatchRequest:
      type: object
      required: [fips_codes]
      properties:
        fips_codes:
          type: array
          items:
            type: string
            pattern: '^\d{12}$'
          minItems: 1
          maxItems: 100
          description: Array of 12-digit block group FIPS codes
        window:
          type: integer
          enum: [7, 14, 30]
          default: 30
        signal_type:
          type: string
          enum: [at-place, residential, combined]
          default: combined
        fields:
          type: array
          items:
            type: string
          description: Optional sparse fieldset — return only specified fields per result

    SignalBatchResponse:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/BlockGroupSignal'
        meta:
          $ref: '#/components/schemas/ResponseMeta'

    ResponseMeta:
      type: object
      required: [request_id, credits_used, credits_remaining, product, version, provenance]
      properties:
        request_id:
          type: string
        credits_used:
          type: integer
        credits_remaining:
          type: integer
        product:
          type: string
          example: popcast.signal
        version:
          type: string
          example: 2.0.0
        provenance:
          type: object
          description: TF-93 provenance block
          properties:
            source:
              type: string
            source_doc:
              type: string
              format: uri
            methodology_version:
              type: string
            data_vintage:
              type: string
            data_freshness:
              type: string
            data_latency_days:
              type: integer
            data_maturity:
              type: string
              enum: [production, synthetic-only, roadmap]

  responses:
    ValidationError:
      description: Validation error
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: object
                properties:
                  code: { type: string, example: VALIDATION_ERROR }
                  message: { type: string }
                  status: { type: integer, example: 400 }
                  request_id: { type: string }
    NotFound:
      description: Block group not found or no signal available
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: object
                properties:
                  code: { type: string, example: NOT_FOUND }
                  message: { type: string }
                  status: { type: integer, example: 404 }
                  request_id: { type: string }
    RoadmapNotAvailable:
      description: Roadmap endpoint — not yet available
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: object
                properties:
                  code: { type: string, example: ROADMAP_NOT_AVAILABLE }
                  message: { type: string }
                  roadmap_issue: { type: string, example: TF-96 }
                  status: { type: integer, example: 501 }
                  request_id: { type: string }
