openapi: 3.1.0

info:
  title: ROLLIN Accessibility API
  version: 1.2.0
  description: |
    The ROLLIN API provides wheelchair accessibility intelligence for 105,000+
    venues across 15 US states. Every location carries a proprietary 0-100
    score built from community verification, expert submissions, and
    multi-source enrichment.

    ## Authentication

    Pass your API key in the `X-Api-Key` request header:

    ```
    X-Api-Key: rld_your_key_here
    ```

    Keys follow the format `rl{tier}_{32 hex chars}` (36 characters total):
    - `f` — Free (self-serve, 90-day expiry)
    - `s` — Starter ($9.99/mo)
    - `d` — Developer ($29/mo)
    - `b` — Business ($149/mo)
    - `e` — Enterprise (custom)

    Get a free API key at https://joinrollin.com/portal.html

    ## Tiers & Rate Limits

    | Tier       | Per Minute | Per Day  | Per Month |
    |------------|-----------|----------|-----------|
    | Free       | 10        | 33       | 1,000     |
    | Starter    | 30        | 167      | 5,000     |
    | Developer  | 60        | 1,667    | 50,000    |
    | Business   | 200       | 16,667   | 500,000   |
    | Enterprise | custom    | custom   | unlimited |

    Rate-limit headers are included on every response:
    - `X-RateLimit-Limit` — requests allowed per minute
    - `X-RateLimit-Remaining` — requests remaining this window
    - `X-RateLimit-Reset` — Unix timestamp when the window resets

    ## Data Gating by Tier

    | Field              | Free | Starter | Developer | Business+ |
    |--------------------|------|---------|-----------|-----------|
    | Name, score, category, distance | yes | yes | yes | yes |
    | Address            | city + state only | full | full | full |
    | Coordinates        | ~1 km precision | full | full | full |
    | Features, environment | hidden | yes | yes | yes |
    | Phone, website, hours | hidden | yes | yes | yes |
    | Verified status    | hidden | yes | yes | yes |
    | Photo URL          | hidden | hidden | yes | yes |
    | Data confidence    | hidden | hidden | hidden | yes |
    | Accessibility tier | yes | yes | yes | yes |
    | Score breakdown    | hidden | hidden | hidden | yes |

    ## Response Envelope

    All endpoints return JSON:

    ```json
    {
      "success": true,
      "data": { ... },
      "attribution": { "text": "Powered by ROLLIN", "url": "https://joinrollin.com" }
    }
    ```

    Errors:

    ```json
    {
      "success": false,
      "error": { "code": "INVALID_PARAMS", "message": "Human-readable description" }
    }
    ```

    ## Score Ranges

    | Label     | Range  |
    |-----------|--------|
    | Excellent | 90-100 |
    | Good      | 70-89  |
    | Fair      | 50-69  |
    | Limited   | 0-49   |

  contact:
    name: ROLLIN Developer Support
    url: https://joinrollin.com/developers.html
    email: support@joinrollin.com
  license:
    name: Proprietary
    url: https://joinrollin.com/api-terms.html

servers:
  - url: https://joinrollin.com/api/v1
    description: Production

tags:
  - name: Core
    description: Search and location endpoints
  - name: Regions
    description: Geographic coverage
  - name: Community
    description: Feedback and data contribution
  - name: Account
    description: API key usage and quota
  - name: System
    description: Health and availability
  - name: Trial
    description: No-auth trial endpoints (rate-limited)

# ─────────────────────────────────────────────────────────────
# Paths
# ─────────────────────────────────────────────────────────────
paths:

  /health:
    get:
      operationId: getHealth
      summary: API health check
      description: Returns API and database status. No authentication required.
      tags: [System]
      security: []
      responses:
        '200':
          description: API is operational or degraded
          headers:
            X-API-Version:
              $ref: '#/components/headers/XApiVersion'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
        '405':
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: API is down
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'

  /locations:
    get:
      operationId: searchLocations
      summary: Search accessible locations
      description: |
        Search for wheelchair-accessible venues near a coordinate. Results
        include a proprietary accessibility score and are sorted by distance
        (default), score, or last-updated date.

        **Free tier:** radius capped at 10 mi, max 10 results, 100 unique
        locations/day. Address reduced to city+state, coordinates rounded to
        ~1 km, features/contact/verified fields hidden.

        **Pagination:** Use `next_cursor` from the previous response to fetch
        the next page.
      tags: [Core]
      security:
        - ApiKeyHeader: []
      parameters:
        - name: lat
          in: query
          required: true
          description: Latitude (continental US, 24.0-50.0)
          schema:
            type: number
            minimum: 24.0
            maximum: 50.0
          example: 40.7128
        - name: lng
          in: query
          required: true
          description: Longitude (continental US, -125.0 to -66.0)
          schema:
            type: number
            minimum: -125.0
            maximum: -66.0
          example: -74.006
        - name: radius
          in: query
          description: Search radius in miles. Free tier capped at 10, all others at 25.
          schema:
            type: number
            default: 5
            minimum: 0.1
            maximum: 25
          example: 5
        - name: limit
          in: query
          description: Max results per page. Free tier capped at 10, all others at 50.
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 50
          example: 20
        - name: min_score
          in: query
          description: Minimum accessibility score (0-100).
          schema:
            type: integer
            default: 0
            minimum: 0
            maximum: 100
          example: 70
        - name: features
          in: query
          description: |
            Comma-separated required features. Only locations with ALL listed
            features confirmed are returned. Values: `wheelchair_entry`,
            `accessible_restroom`, `level_entry`, `parking`, `wide_aisles`,
            `elevator`.
          schema:
            type: string
          example: wheelchair_entry,accessible_restroom
        - name: cuisine
          in: query
          description: Filter by cuisine type (partial match).
          schema:
            type: string
          example: italian
        - name: sort
          in: query
          description: Sort order.
          schema:
            type: string
            enum: [distance, score, updated]
            default: distance
        - name: lighting
          in: query
          description: Filter by lighting level. Silently ignored on Free tier.
          schema:
            type: string
            enum: [bright, moderate, dim]
        - name: min_confidence
          in: query
          description: Minimum data confidence. **Business+ only** (403 otherwise).
          schema:
            type: string
            enum: [high, medium, low]
        - name: cursor
          in: query
          description: Opaque pagination cursor from `next_cursor`.
          schema:
            type: string
      responses:
        '200':
          description: Search results
          headers:
            X-API-Version:
              $ref: '#/components/headers/XApiVersion'
            X-Scoring-Version:
              $ref: '#/components/headers/XScoringVersion'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    const: true
                  count:
                    type: integer
                    description: Results in this page
                  next_cursor:
                    type: string
          
                    description: Cursor for next page, null when done
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/Location'
                  attribution:
                    $ref: '#/components/schemas/Attribution'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'

  /locations/{id}:
    get:
      operationId: getLocation
      summary: Get a single location
      description: |
        Retrieve full details for a location by UUID, OSM ID (`osm-12345`),
        or Google Place ID (`google-ChIJ...`).
      tags: [Core]
      security:
        - ApiKeyHeader: []
      parameters:
        - name: id
          in: path
          required: true
          description: Location UUID, OSM ID, or Google Place ID
          schema:
            type: string
          example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
      responses:
        '200':
          description: Location found
          headers:
            X-API-Version:
              $ref: '#/components/headers/XApiVersion'
            X-Scoring-Version:
              $ref: '#/components/headers/XScoringVersion'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    const: true
                  location:
                    $ref: '#/components/schemas/LocationDetail'
                  attribution:
                    $ref: '#/components/schemas/Attribution'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'

  /regions:
    get:
      operationId: listRegions
      summary: List coverage regions
      description: |
        Returns all geographic regions with location counts and coverage tier.
        Response is cached for 1 hour.
      tags: [Regions]
      security:
        - ApiKeyHeader: []
      responses:
        '200':
          description: Region list
          headers:
            Cache-Control:
              schema:
                type: string
                example: public, max-age=3600
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    const: true
                  count:
                    type: integer
                  regions:
                    type: array
                    items:
                      $ref: '#/components/schemas/Region'
                  total_locations:
                    type: integer
                  attribution:
                    $ref: '#/components/schemas/Attribution'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'

  /score/{id}:
    get:
      operationId: getScore
      summary: Get scoring breakdown
      description: |
        Feature-by-feature explanation of a location's score with qualitative
        impact levels. **Business+ only.**
      tags: [Core]
      security:
        - ApiKeyHeader: []
      parameters:
        - name: id
          in: path
          required: true
          description: Location UUID, OSM ID, or Google Place ID
          schema:
            type: string
          example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
      responses:
        '200':
          description: Score breakdown
          headers:
            X-Scoring-Version:
              $ref: '#/components/headers/XScoringVersion'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    const: true
                  location_id:
                    type: string
                    format: uuid
                  location_name:
                    type: string
                  score:
                    type: integer
                    minimum: 0
                    maximum: 100
                  score_label:
                    type: string
                    enum: [Excellent, Good, Fair, Limited]
                  accessibility_tier:
                    type: string
                    enum: [Fully Accessible, Accessible, Limited]
                  verified:
                    type: boolean
                  scoring:
                    type: object
                    properties:
                      features:
                        type: object
                        additionalProperties:
                          $ref: '#/components/schemas/FeatureBreakdownItem'
                      notes:
                        type: array
                        items:
                          type: string
                  methodology:
                    $ref: '#/components/schemas/ScoringMethodology'
                  attribution:
                    $ref: '#/components/schemas/Attribution'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'

  /feedback:
    post:
      operationId: submitFeedback
      summary: Submit accessibility feedback
      description: |
        Submit a correction or verification for a location. **Developer+ only.**

        Types:
        - `accurate` — confirm data is correct
        - `inaccurate` — flag data as wrong
        - `correction` — provide updated feature values
      tags: [Community]
      security:
        - ApiKeyHeader: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FeedbackRequest'
            examples:
              correction:
                summary: Correct a feature
                value:
                  location_id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
                  feedback_type: correction
                  features:
                    accessible_restroom: true
                    wheelchair_entry: true
                  comment: Ramp added in 2025 renovation.
              confirm:
                summary: Confirm accuracy
                value:
                  location_id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
                  feedback_type: accurate
      responses:
        '201':
          description: Feedback accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    const: true
                  feedback:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      location_id:
                        type: string
                        format: uuid
                      location_name:
                        type: string
                      feedback_type:
                        type: string
                        enum: [accurate, inaccurate, correction]
                      status:
                        type: string
                        const: pending
                      created_at:
                        type: string
                        format: date-time
                  message:
                    type: string
                  attribution:
                    $ref: '#/components/schemas/Attribution'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'

  /usage:
    get:
      operationId: getUsage
      summary: Get API key usage and quota
      description: |
        Returns live usage counters and remaining quota for the authenticated
        key. Never cached.
      tags: [Account]
      security:
        - ApiKeyHeader: []
      responses:
        '200':
          description: Usage data
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    const: true
                  tier:
                    type: string
                    enum: [free, starter, developer, business, enterprise]
                  usage:
                    type: object
                    properties:
                      minute:
                        $ref: '#/components/schemas/UsageWindow'
                      day:
                        $ref: '#/components/schemas/UsageWindow'
                      month:
                        $ref: '#/components/schemas/UsageWindow'
                      unique_locations:
                        description: Free tier only — daily unique location cap
              
                        allOf:
                          - $ref: '#/components/schemas/UsageWindow'
                  attribution:
                    $ref: '#/components/schemas/Attribution'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  # ── Trial endpoints (no auth) ───────────────────────────────
  /trial/health:
    get:
      operationId: getTrialHealth
      summary: Trial health check
      description: Returns trial endpoint status and rate limits. No auth required.
      tags: [Trial]
      security: []
      responses:
        '200':
          description: Trial is operational
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    const: ok
                  endpoint:
                    type: string
                    const: trial
                  limits:
                    type: object
                    properties:
                      max_radius_miles:
                        type: integer
                        const: 5
                      max_results:
                        type: integer
                        const: 5
                      rate_per_minute:
                        type: integer
                        const: 5
                      rate_per_day:
                        type: integer
                        const: 30
                  upgrade:
                    type: string
                  trial:
                    type: boolean
                    const: true
        '429':
          $ref: '#/components/responses/RateLimited'

  /trial/locations:
    get:
      operationId: searchTrialLocations
      summary: Trial location search
      description: |
        Search locations without an API key. Limited to 5 results, 5 mi radius,
        5 req/min, 30 req/day. Requires `X-Trial-Session` header (32-char hex).
      tags: [Trial]
      security: []
      parameters:
        - name: X-Trial-Session
          in: header
          required: true
          description: 32-character hex session ID
          schema:
            type: string
            pattern: '^[a-f0-9]{32}$'
        - name: lat
          in: query
          required: true
          schema:
            type: number
            minimum: 24.0
            maximum: 50.0
        - name: lng
          in: query
          required: true
          schema:
            type: number
            minimum: -125.0
            maximum: -66.0
        - name: radius
          in: query
          schema:
            type: number
            default: 5
            maximum: 5
        - name: limit
          in: query
          schema:
            type: integer
            default: 5
            maximum: 5
        - name: min_score
          in: query
          schema:
            type: integer
            minimum: 0
            maximum: 100
        - name: q
          in: query
          description: Search term (name or category)
          schema:
            type: string
        - name: features
          in: query
          schema:
            type: string
        - name: lighting
          in: query
          schema:
            type: string
            enum: [bright, moderate, dim]
      responses:
        '200':
          description: Trial search results
          content:
            application/json:
              schema:
                type: object
                properties:
                  locations:
                    type: array
                    items:
                      $ref: '#/components/schemas/TrialLocation'
                  total:
                    type: integer
                  query:
                    type: object
                    properties:
                      lat:
                        type: number
                      lng:
                        type: number
                      radius:
                        type: number
                      limit:
                        type: integer
                      min_score:
                        type: integer
                  trial:
                    type: boolean
                    const: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'

  /trial/locations/{id}:
    get:
      operationId: getTrialLocation
      summary: Trial location detail
      description: Get a single location via trial. Requires `X-Trial-Session` header.
      tags: [Trial]
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: X-Trial-Session
          in: header
          required: true
          schema:
            type: string
            pattern: '^[a-f0-9]{32}$'
      responses:
        '200':
          description: Location found
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/TrialLocation'
                  - type: object
                    properties:
                      trial:
                        type: boolean
                        const: true
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /trial/score/{id}:
    get:
      operationId: getTrialScore
      summary: Trial score breakdown
      description: Basic score info via trial. Requires `X-Trial-Session` header.
      tags: [Trial]
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: X-Trial-Session
          in: header
          required: true
          schema:
            type: string
            pattern: '^[a-f0-9]{32}$'
      responses:
        '200':
          description: Score data
          content:
            application/json:
              schema:
                type: object
                properties:
                  score:
                    type: integer
                    minimum: 0
                    maximum: 100
                  label:
                    type: string
                    enum: [Excellent, Good, Fair, Limited]
                  tier:
                    type: string
                    enum: [Fully Accessible, Accessible, Limited]
                  features:
                    type: object
                    description: |
                      Raw feature values using database column names.
                      Note: uses `accessible_parking` (not `parking` as in search results).
                    properties:
                      wheelchair_entry:
                        type: boolean
                      accessible_restroom:
                        type: boolean
                      level_entry:
                        type: boolean
                      accessible_parking:
                        type: boolean
                      elevator:
                        type: boolean
                      wide_aisles:
                        type: boolean
                  trial:
                    type: boolean
                    const: true
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /trial/regions:
    get:
      operationId: listTrialRegions
      summary: Trial region list
      description: List regions via trial. Requires `X-Trial-Session` header.
      tags: [Trial]
      security: []
      parameters:
        - name: X-Trial-Session
          in: header
          required: true
          schema:
            type: string
            pattern: '^[a-f0-9]{32}$'
      responses:
        '200':
          description: Region list
          content:
            application/json:
              schema:
                type: object
                properties:
                  count:
                    type: integer
                  total_locations:
                    type: integer
                  regions:
                    type: array
                    items:
                      type: object
                      properties:
                        region:
                          type: string
                        state:
                          type: string
                        location_count:
                          type: integer
                  trial:
                    type: boolean
                    const: true
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'

  /trial/feedback:
    post:
      operationId: submitTrialFeedback
      summary: Trial feedback submission
      description: Submit feedback via trial. Requires `X-Trial-Session` header. Comment max 500 chars.
      tags: [Trial]
      security: []
      parameters:
        - name: X-Trial-Session
          in: header
          required: true
          schema:
            type: string
            pattern: '^[a-f0-9]{32}$'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [location_id, feedback_type]
              properties:
                location_id:
                  type: string
                  format: uuid
                feedback_type:
                  type: string
                  enum: [accurate, inaccurate, correction]
                features:
                  $ref: '#/components/schemas/Features'
                comment:
                  type: string
                  maxLength: 500
      responses:
        '200':
          description: Feedback accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  submitted:
                    type: boolean
                    const: true
                  message:
                    type: string
                  trial:
                    type: boolean
                    const: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'

# ─────────────────────────────────────────────────────────────
# Components
# ─────────────────────────────────────────────────────────────
components:

  securitySchemes:
    ApiKeyHeader:
      type: apiKey
      in: header
      name: X-Api-Key
      description: ROLLIN API key

  headers:
    XApiVersion:
      description: API version
      schema:
        type: string
        example: '1.2.0'
    XScoringVersion:
      description: Scoring engine version
      schema:
        type: string
        example: '3.0.2'
    XRateLimitLimit:
      description: Requests allowed per minute
      schema:
        type: integer
    XRateLimitRemaining:
      description: Requests remaining this window
      schema:
        type: integer

  schemas:

    # ── Location (search result) ──────────────────────────────
    Location:
      type: object
      description: |
        Venue with accessibility data. Fields are tier-gated:
        - Free: name, score, category, distance, approximate address/coords
        - Starter/Developer: + features, environment, phone, website, hours, verified
        - Business+: + photo_url, data_confidence
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          example: The Beacon Hotel
        address:
          type: string
          description: Full address (Starter+) or city+state (Free)
          example: 2130 Broadway, New York, NY 10023
        neighborhood:
          type: string

          example: Upper West Side
        lat:
          type: number
          description: Full precision (Starter+) or ~1 km (Free)
          example: 40.7845
        lng:
          type: number
          example: -73.9815
        score:
          type: integer
          minimum: 0
          maximum: 100
          example: 87
        score_label:
          type: string
          enum: [Excellent, Good, Fair, Limited]
        accessibility_tier:
          type: string
          enum: [Fully Accessible, Accessible, Limited]
          description: |
            Based on restroom availability:
            - Fully Accessible = accessible restroom confirmed (can stay as long as needed)
            - Accessible = can enter but no confirmed accessible restroom
            - Limited = score below 50 / major barriers
        category:
          type: string

          example: hotel
        distance_miles:
          type: number
          example: 0.4
        features:
          description: Starter+ only

          $ref: '#/components/schemas/Features'
        environment:
          description: Starter+ only

          $ref: '#/components/schemas/Environment'
        verified:
          type: boolean

          description: Starter+ only
        last_updated:
          type: string
          format: date-time

          description: Starter+ only
        phone:
          type: string

          description: Starter+ only
        website:
          type: string
          format: uri

          description: Starter+ only
        hours:
          type: string

          description: Starter+ only
        # photo_url and data_confidence only appear on /locations/:id (LocationDetail)
        # They are never included in list search results.

    # ── Location Detail (single location) ─────────────────────
    LocationDetail:
      allOf:
        - $ref: '#/components/schemas/Location'
        - type: object
          properties:
            verified_at:
              type: string
              format: date-time
            photo_url:
              type: string
              format: uri
              description: Developer+ only — first photo URL if available
            data_confidence:
              type: string
              enum: [high, medium, low]
              description: |
                Business+ only. Calculated as:
                - high = verified by ROLLIN team
                - medium = 3+ accessibility features known
                - low = fewer than 3 features known
    

    # ── Features ──────────────────────────────────────────────
    Features:
      type: object
      description: |
        `true` = confirmed present, `false` = confirmed absent, `null` = unknown.
      properties:
        wheelchair_entry:
          type: boolean

        accessible_restroom:
          type: boolean

        level_entry:
          type: boolean

        parking:
          type: boolean

        wide_aisles:
          type: boolean

        elevator:
          type: boolean


    # ── Environment ───────────────────────────────────────────
    Environment:
      type: object
      properties:
        lighting_level:
          type: string

          enum: [bright, moderate, dim]
        fluorescent_lighting:
          type: boolean


    # ── Trial Location ────────────────────────────────────────
    TrialLocation:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        address:
          type: string
        neighborhood:
          type: string

        category:
          type: string
        lat:
          type: number
        lng:
          type: number
        score:
          type: integer
          minimum: 0
          maximum: 100
        score_label:
          type: string
        tier:
          type: string
        distance_miles:
          type: number
        features:
          $ref: '#/components/schemas/Features'
        environment:
          $ref: '#/components/schemas/Environment'

    # ── Score Breakdown Item ──────────────────────────────────
    FeatureBreakdownItem:
      type: object
      properties:
        value:
          type: string
          enum: [yes, no, unknown]
        impact:
          type: string
          enum: [high_positive, positive, not_scored, negative, high_negative]
        critical:
          type: boolean

    # ── Scoring Methodology ───────────────────────────────────
    ScoringMethodology:
      type: object
      properties:
        version:
          type: string
          example: '3.0.2'
        description:
          type: string
        impact_scale:
          type: array
          items:
            type: string
        score_ranges:
          type: object
          additionalProperties:
            type: string

    # ── Region ────────────────────────────────────────────────
    Region:
      type: object
      properties:
        state:
          type: string
          description: Two-letter US state abbreviation
          example: NY
        location_count:
          type: integer
          example: 21000
        coverage:
          type: string
          enum: [primary, secondary, emerging]
          description: |
            primary = 10,000+, secondary = 1,000-9,999, emerging = under 1,000

    # ── Feedback Request ──────────────────────────────────────
    FeedbackRequest:
      type: object
      required: [location_id, feedback_type]
      properties:
        location_id:
          type: string
          format: uuid
        feedback_type:
          type: string
          enum: [accurate, inaccurate, correction]
        features:
          description: Required when feedback_type is `correction`
          $ref: '#/components/schemas/Features'
        comment:
          type: string
          maxLength: 1000

    # ── Usage Window ──────────────────────────────────────────
    UsageWindow:
      type: object
      properties:
        used:
          type: integer
        limit:
          type: integer
        remaining:
          type: integer

    # ── Health Response ───────────────────────────────────────
    HealthResponse:
      type: object
      properties:
        status:
          type: string
          enum: [operational, degraded, down]
        version:
          type: string
        timestamp:
          type: string
          format: date-time
        response_time_ms:
          type: integer
        services:
          type: object
          properties:
            api:
              type: string
              enum: [healthy]
            database:
              type: string
              enum: [healthy, degraded, down, unknown]
        data:
          type: object
          properties:
            locations:
              type: integer
    

    # ── Attribution ───────────────────────────────────────────
    Attribution:
      type: object
      properties:
        text:
          type: string
          const: Powered by ROLLIN
        url:
          type: string
          format: uri
          const: https://joinrollin.com

    # ── Error ─────────────────────────────────────────────────
    Error:
      type: object
      properties:
        success:
          type: boolean
          const: false
        error:
          type: object
          properties:
            code:
              type: string
            message:
              type: string

  # ── Reusable Responses ──────────────────────────────────────
  responses:
    BadRequest:
      description: Invalid request parameters
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: Tier insufficient for this endpoint or parameter
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: Rate limit exceeded
      headers:
        Retry-After:
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ServerError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
