openapi: 3.1.0
info:
  title: BDS Chat - Vietnam Real Estate Due-Diligence API
  description: |
    The accurate, standardized data layer for Vietnamese real estate. Government-sourced truth — zoning, official land prices, admin boundaries, legal references — enriched to international standards (RESO, ISO, GeoJSON, LBCS).

    **What this API provides:**
    - Official government land prices (NQ 87/2025, 30,775 street-level records)
    - Zoning and planning data from HCMC Planning API (zone codes, DGT road risk, LBCS mapping)
    - Admin boundaries (312 wards + 22 districts) with 2025 ward reform mapping
    - Listing search with quality, premium, and risk scoring (transparent breakdowns)
    - Document verification pipeline (sổ hồng/sổ đỏ)
    - Legal references (decisions, decrees) with provenance
    - Coordinate-based risk assessment (standalone, not tied to a listing)

    **Coverage:** Hồ Chí Minh City (HCMC). Expanding to other Vietnamese cities only when the full data foundation exists.

    **Standards:** RESO Data Dictionary, ISO 4217 (VND), ISO 3166-2:VN (VN-SG), ISO 8601 dates, GeoJSON RFC 7946 (EPSG:4326), APA LBCS zoning codes, W3C PROV provenance.

    **Currency:** All prices in VND (Vietnamese Dong).
    - 1 tỷ = 1,000,000,000 VND
    - 1 triệu = 1,000,000 VND

    **Direction codes:** N, S, E, W, NE, SE, NW, SW

    **Legal status codes:**
    - `so_do_so_hong` — Sổ đỏ/Sổ hồng (full ownership)
    - `so_hong` — Sổ hồng
    - `so_hong_rieng` — Sổ hồng riêng (individual title)
    - `dang_cho_so` — Đang chờ sổ (pending title)
    - `giay_tay` — Giấy tay (handwritten deed, informal)

    **Road facing codes:**
    - `mat_tien` — Mặt tiền (street-facing, highest value)
    - `hem_xe_hoi` — Hẻm xe hơi (car alley, >=4m)
    - `hem_xe_may` — Hẻm xe máy (motorbike alley, <4m)
    - `hem_cut` — Hẻm cụt (dead-end alley)

    **Structure types:**
    - `dat_nen` — Vacant land
    - `nha_cap_4` — Single story house
    - `nha_pho` — Townhouse (2-5 floors)
    - `biet_thu` — Villa
    - `can_ho` — Apartment/condo
    - `nha_xuong` — Warehouse/factory

    **Land types:**
    - `tho_cu` — Residential (can build)
    - `nong_nghiep` — Agricultural (cannot build without conversion)
    - `hon_hop` — Mixed (partial residential)

    **Seller types:**
    - `chinh_chu` — Direct owner
    - `moi_gioi` — Broker/agent

    ---

    ## Persona Workflows

    **B2B personas** — platforms, developers, and brokers who build on or contribute to the platform.
    **B2C personas** — buyers searching and owners selling property.
    Government data (prices, zoning, boundaries, legal refs) is a platform capability available to all personas, not a separate persona.

    ### B2B: Platform — Sync → Enrich → Manage
    ```
    # 1. Register for a partner API key
    POST /auth/register
    {"email":"ops@myplatform.vn","name":"My RE Platform","tier":"partner"}

    # 2. Sync a listing with your platform ID
    POST /listings
    {"title":"...","source":"myplatform","source_listing_id":"PLT-12345",...}

    # 3. Duplicate returns 409 (dedup on source + source_listing_id)
    POST /listings  → 409 Conflict

    # 4. Normalize ward names before import (2025 reform aware)
    GET /meta/resolve-ward?ward=Phuong+15&district=Binh+Tan

    # 5. Page your synced listings by update time
    GET /listings?sort_by=updated&sort_order=desc&limit=50&offset=0

    # 6. Enrich with gov price comparison
    GET /meta/gov-land-price/compare?listing_id={id}

    # 7. Enrich with zoning and risk
    GET /meta/zoning/check-listing?listing_id={id}
    GET /meta/risk/check?lat=10.7228&lon=106.6105
    ```

    ### B2B: Developer — Register → Auth → Build
    ```
    # 1. Register for an API key
    POST /auth/register
    {"email":"dev@myapp.com","name":"My RE App","tier":"free"}
    # Response: {"api_key":"bds_live_...","tier":"free","daily_limit":100}

    # 2. Use key in all subsequent requests
    # Header: Authorization: Bearer bds_live_...

    # 3. Search listings with filters
    GET /listings?district=Binh+Tan&max_price=5000000000&limit=20

    # 4. Get boundaries for map rendering
    GET /meta/boundaries?level=district&format=geojson
    GET /meta/boundaries?level=ward&district_code=769&format=geojson

    # 5. Reverse geocode a point
    GET /meta/boundaries/contains?lat=10.7228&lon=106.6105

    # 6. Gov data enrichment (prices, zoning, legal refs)
    GET /meta/gov-land-price?street=Ten+Lua&district=Binh+Tan
    GET /meta/zoning?lat=10.7228&lon=106.6105
    GET /meta/legal-refs?topic=land_price

    # 7. Check usage
    GET /auth/usage
    ```

    ### B2B: Broker — Create → Manage → Verify
    ```
    # 1. Register as a broker
    POST /auth/register
    {"email":"agent@agency.vn","name":"Minh Nguyen","tier":"pro"}

    # 2. Create a listing
    POST /listings
    {"title":"Đất thổ cư Bình Tân","price_vnd":3000000000,"area_m2":80,"district":"Bình Tân","source":"api"}

    # 3. Upload listing photos
    POST /images  (multipart/form-data, file=photo.jpg)

    # 4. Submit legal document (sổ hồng)
    POST /listings/{id}/documents
    {"doc_type":"so_hong","doc_serial":"CT 12345678","doc_area_m2":80}

    # 5. Upload document photo
    POST /listings/{id}/documents/{doc_id}/images  (multipart/form-data, file=sohong.jpg)

    # 6. Check verification status
    GET /listings/{id}/documents

    # 7. Compare price to gov benchmark
    GET /meta/gov-land-price/compare?listing_id={id}
    ```

    ### B2C: Buyer — Search → Evaluate → Verify
    ```
    # 1. Search listings
    GET /listings?district=Binh+Tan&max_price=5000000000&limit=10

    # 2. Get full detail (includes scores + provenance)
    GET /listings/{id}

    # 3. Compare price to government benchmark
    GET /meta/gov-land-price/compare?listing_id={id}

    # 4. Check zoning and risk
    GET /meta/zoning/check-listing?listing_id={id}
    GET /meta/risk/check?lat=10.7228&lon=106.6105

    # 5. Look up legal references
    GET /meta/legal-refs?topic=land_price

    # 6. Check ward reform status
    GET /meta/reform-search?q=Binh+Tan
    ```

    ### B2C: Owner — Post → Document → Track
    ```
    # 1. Create your listing
    POST /listings
    {"title":"Bán nhà Quận 7","price_vnd":5500000000,"area_m2":65,"district":"Quận 7","source":"api","seller_type":"chinh_chu"}

    # 2. Upload photos
    POST /images  (multipart/form-data, file=photo.jpg)

    # 3. Submit ownership document
    POST /listings/{id}/documents
    {"doc_type":"so_hong","doc_serial":"CT 12345678","doc_area_m2":65}

    # 4. Check how your listing compares to gov benchmark
    GET /meta/gov-land-price/compare?listing_id={id}

    # 5. Verify zoning for your property
    GET /meta/zoning/check-listing?listing_id={id}
    ```

    ### Authentication
    API supports optional API key authentication via Bearer token.
    Register for a key at `POST /auth/register`.
    Include in requests: `Authorization: Bearer bds_live_...`

    **Access tiers:**
    | Tier | Daily Limit | Rate Limit | Auth Required |
    |------|------------|------------|---------------|
    | Free | 100 req/day | 5 req/sec | API key |
    | Pro | 10,000 req/day | 50 req/sec | API key |
    | Partner | 100,000 req/day | 200 req/sec | API key |

    All data endpoints require authentication. Public endpoints (health, docs, openapi.yaml, auth/register) are accessible without a key.

    Rate limit headers on every response: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`.

    Every response includes `X-Request-ID` for traceability. Send your own `X-Request-ID` header to correlate requests, or one will be generated (UUID v7).

    ### Current Limitations
    - No update/unpublish CRUD (listings cannot be edited or removed via API)
    - No agent inventory filter (cannot list all listings by agent_id)
  version: 4.2.0
servers:
  - url: https://api.bds.chat
    description: Production
externalDocs:
  description: Interactive API Documentation
  url: https://api.bds.chat/docs
tags:
  - name: Authentication
    description: API key registration, usage tracking, and rate limit management
  - name: Listings
    description: Search, view, and create real estate listings
  - name: Documents
    description: Document verification for listings (land titles, ownership proof)
  - name: Images
    description: Image proxy and upload for listing photos
  - name: Government Data
    description: Official government land prices, zoning, and legal references
  - name: Geography
    description: Administrative boundaries, ward resolution, and reform data
  - name: Admin
    description: Administrative operations (document review, verification)
  - name: System
    description: Health checks, API specification, and root endpoint
security:
  - BearerAuth: []
paths:
  /auth/register:
    post:
      operationId: registerAPIKey
      tags: [Authentication]
      summary: Register for an API key
      security: []
      description: |
        Create a new API key for authenticated access.
        The raw API key is returned once — save it securely.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  description: Email address for the API key owner
                name:
                  type: string
                  description: App or project name
                tier:
                  type: string
                  enum: [free, pro, partner]
                  description: Access tier (default free)
            example:
              email: developer@example.com
              name: My RE App
              tier: free
      responses:
        '201':
          description: API key created
          content:
            application/json:
              schema:
                type: object
                properties:
                  api_key:
                    type: string
                    description: Raw API key (save this — shown once)
                  user_id:
                    type: string
                    description: Assigned user ID
                  role:
                    type: string
                    enum: [platform, developer, broker]
                  status:
                    type: string
                    enum: [pending, approved, suspended]
                    description: Account status. New accounts start as "pending" until approved.
                  tier:
                    type: string
                  daily_limit:
                    type: integer
                  created_at:
                    type: string
                  note:
                    type: string
              example:
                api_key: bds_live_a1b2c3d4e5f6...
                user_id: usr_019e5a12
                role: developer
                status: pending
                tier: free
                daily_limit: 100
                created_at: "2026-05-26T12:00:00Z"
                note: "Account is pending approval. Save this API key — it will not be shown again."
        '400':
          description: Invalid request (missing email, invalid tier, etc.)
        '409':
          description: Email already registered

  /auth/usage:
    get:
      operationId: getAPIUsage
      tags: [Authentication]
      summary: Check API key usage
      description: Returns daily usage stats for the authenticated API key.
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Usage statistics
          content:
            application/json:
              schema:
                type: object
                properties:
                  tier:
                    type: string
                  daily_limit:
                    type: integer
                  used_today:
                    type: integer
                  endpoints:
                    type: object
                    description: Per-endpoint usage breakdown for today
                  resets_at:
                    type: string
                    description: ISO 8601 timestamp when daily counter resets
              example:
                tier: free
                daily_limit: 100
                used_today: 42
                endpoints:
                  /listings: 25
                  /meta/zoning: 10
                  /meta/gov-land-price: 7
                resets_at: "2026-05-25T00:00:00Z"
        '401':
          description: API key required

  /listings:
    get:
      operationId: searchListings
      tags: [Listings]
      summary: Search real estate listings with filters
      description: |
        Returns property listings matching the given filters.
        All filter parameters are optional — omit to get all listings.
        Prices are in VND. Use 1000000000 for 1 tỷ, 5000000000 for 5 tỷ.
      parameters:
        - name: city
          in: query
          schema:
            type: string
          description: City name. Diacritics optional — "Ho Chi Minh" and "Hồ Chí Minh" both work.
        - name: district
          in: query
          schema:
            type: string
          description: District name. Diacritics optional — "Binh Tan" and "Bình Tân" both work.
        - name: ward
          in: query
          schema:
            type: string
          description: Ward name. Diacritics optional — "Tan Tao" and "Tân Tạo" both work.
        - name: min_price
          in: query
          schema:
            type: integer
          description: Minimum total price in VND (e.g., 3000000000 = 3 tỷ)
        - name: max_price
          in: query
          schema:
            type: integer
          description: Maximum total price in VND (e.g., 5000000000 = 5 tỷ)
        - name: min_area
          in: query
          schema:
            type: number
          description: Minimum land area in m²
        - name: max_area
          in: query
          schema:
            type: number
          description: Maximum land area in m²
        - name: min_price_m2
          in: query
          schema:
            type: integer
          description: Minimum price per m² in VND
        - name: max_price_m2
          in: query
          schema:
            type: integer
          description: Maximum price per m² in VND
        - name: direction
          in: query
          schema:
            type: string
            enum: [N, S, E, W, NE, SE, NW, SW]
          description: House facing direction
        - name: legal_status
          in: query
          schema:
            type: string
            enum: [so_do_so_hong, so_hong, so_hong_rieng, dang_cho_so, giay_tay]
          description: Legal ownership status
        - name: min_frontage
          in: query
          schema:
            type: number
          description: Minimum street frontage in meters
        - name: listing_type
          in: query
          schema:
            type: string
            enum: [dat, nha, canho]
          description: Property type (dat=land, nha=house, canho=apartment)
        - name: source
          in: query
          schema:
            type: string
            enum: [alonhadat, batdongsan, api]
          description: Data source
        - name: price_tier
          in: query
          schema:
            type: string
            enum: [central, inner_urban, periurban, satellite]
          description: Price tier based on ward location (central=Q1/Q3/Q5, inner_urban=Bình Tân/Gò Vấp, periurban=Bình Chánh/Hóc Môn, satellite=ex-Bình Dương/BRVT)
        - name: road_facing
          in: query
          schema:
            type: string
            enum: [mat_tien, hem_xe_hoi, hem_xe_may, hem_cut]
          description: Road access type (mat_tien=street-facing, hem_xe_hoi=car alley, hem_xe_may=motorbike alley, hem_cut=dead-end)
        - name: structure_type
          in: query
          schema:
            type: string
            enum: [dat_nen, nha_cap_4, nha_pho, biet_thu, can_ho, nha_xuong]
          description: Building structure type
        - name: land_type
          in: query
          schema:
            type: string
            enum: [tho_cu, nong_nghiep, hon_hop]
          description: Land use classification
        - name: seller_type
          in: query
          schema:
            type: string
            enum: [chinh_chu, moi_gioi]
          description: Seller type (chinh_chu=direct owner, moi_gioi=broker)
        - name: min_bedrooms
          in: query
          schema:
            type: integer
          description: Minimum number of bedrooms
        - name: min_bathrooms
          in: query
          schema:
            type: integer
          description: Minimum number of bathrooms
        - name: min_alley_width
          in: query
          schema:
            type: number
          description: Minimum alley width in meters (for hẻm properties)
        - name: min_quality
          in: query
          schema:
            type: number
            minimum: 0
            maximum: 1
          description: Minimum quality score (0-1). Default 0.4 filters out incomplete listings. Pass 0 to include all.
        - name: min_premium
          in: query
          schema:
            type: number
            minimum: 0
            maximum: 1
          description: Minimum premium score (0-1). Measures how premium the listing characteristics are. No default filter — omit to include all.
        - name: max_risk
          in: query
          schema:
            type: number
            minimum: 0
            maximum: 1
          description: Maximum risk score (0-1). Filters out listings with risk above this threshold. No default — omit to include all. Use 0.75 to hide critical-risk listings.
        - name: standard_status
          in: query
          schema:
            type: string
            enum: [Active, Pending, Sold, Expired, Withdrawn]
          description: RESO StandardStatus lifecycle filter. Default shows all statuses. Use "Active" for current listings only.
        - name: sort_by
          in: query
          schema:
            type: string
            enum: [price, area, price_m2, frontage, posted_date, bedrooms, floors, alley_width, updated, quality_score, premium_score, risk_score]
          description: Sort field (default is price)
        - name: sort_order
          in: query
          schema:
            type: string
            enum: [asc, desc]
          description: Sort direction (default is asc)
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
          description: Number of results to return (max 100)
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
          description: Pagination offset
        - name: include
          in: query
          schema:
            type: string
          description: |
            Comma-separated list of optional data overlays to embed per listing.
            - `zoning` — zoning classification, risk level, DGT overlap, LBCS code (from cached gov data)
            - `gov_price` — government land price benchmark with market-to-gov ratio
            Example: `include=zoning,gov_price`. Only available for listings with coordinates (zoning) or gov price linkage (gov_price).
      responses:
        "200":
          description: Matching listings
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListingResponse"

    post:
      operationId: createListing
      tags: [Listings]
      summary: Create a listing (returns 409 on duplicate)
      description: |
        Submit a listing via JSON. ID is a server-generated UUID v7.
        Dedup key: (source, source_listing_id). Returns 409 if duplicate.
        Price fields (price_billion, price_per_m2_vnd, price_per_m2_million) auto-computed from price_vnd and area_m2.
        data_quality auto-computed if not provided.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateListingRequest"
      responses:
        "201":
          description: Listing created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Listing"
        "400":
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Duplicate listing (source + source_listing_id already exists)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /listings/{id}:
    get:
      operationId: getListingDetail
      tags: [Listings]
      summary: Get full details of a specific listing
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Listing ID
      responses:
        "200":
          description: Listing detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Listing"
        "404":
          description: Listing not found
    put:
      operationId: updateListing
      tags: [Listings]
      summary: Update an existing listing (owner only)
      description: |
        Partial update — only provided fields are changed. Requires the same API key that created the listing (or admin).
        Re-runs the enrichment pipeline: ward resolution, gov price matching, quality/premium/risk scoring.
        Price changes are tracked (previous_price_vnd, price_changed_at).
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Listing ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ListingInput"
      responses:
        "200":
          description: Updated listing
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Listing"
        "400":
          description: Invalid input
        "403":
          description: Not the owner of this listing
        "404":
          description: Listing not found
    delete:
      operationId: deleteListing
      tags: [Listings]
      summary: Soft-delete a listing (owner only)
      description: Sets deleted_at and standard_status=Deleted. Requires ownership.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Listing deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  status:
                    type: string
        "403":
          description: Not the owner
        "404":
          description: Listing not found

  /listings/{id}/trust-report:
    get:
      operationId: getTrustReport
      tags: [Intelligence]
      summary: Trust report for a single listing
      description: |
        Returns a structured trust report with overall grade, government price comparison,
        zoning compliance, legal provenance, risk assessment, and data completeness —
        each field citing its data source. Data gaps produce explicit warnings.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Trust report
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TrustReport"
        "404":
          description: Listing not found

  /listings/{id}/status:
    patch:
      operationId: patchListingStatus
      tags: [Listings]
      summary: Atomically transition listing status (owner only)
      description: |
        Atomic compare-and-swap status transition. Returns 409 if the listing was modified concurrently.
        Valid transitions: Active→Pending/Expired/Withdrawn/Deleted, Pending→Active/Closed/Withdrawn, etc.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status]
              properties:
                status:
                  type: string
                  enum: [Active, Pending, Closed, Expired, Withdrawn, Deleted]
      responses:
        "200":
          description: Status updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  previous_status:
                    type: string
                  status:
                    type: string
        "400":
          description: Invalid status transition
        "403":
          description: Not the owner
        "409":
          description: Concurrent modification — retry

  /listings/batch:
    post:
      operationId: batchUpsertListings
      tags: [Listings]
      summary: Batch create/update listings (authenticated, 10MB limit)
      description: |
        Upserts up to 100 listings per request. Source and data_class are server-owned.
        Deduplication by (user_id, source, source_listing_id). Intra-request duplicate source_listing_ids are rejected.
        Request body limited to 10MB.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [listings]
              properties:
                listings:
                  type: array
                  items:
                    $ref: "#/components/schemas/ListingInput"
      responses:
        "207":
          description: Multi-status response
          content:
            application/json:
              schema:
                type: object
                properties:
                  total:
                    type: integer
                  created:
                    type: integer
                  updated:
                    type: integer
                  rejected:
                    type: integer
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        index:
                          type: integer
                        id:
                          type: string
                        status:
                          type: string
                        error:
                          type: string

  /auth/profile:
    get:
      operationId: getProfile
      tags: [Authentication]
      summary: Get user profile and API keys
      responses:
        "200":
          description: User profile
          content:
            application/json:
              schema:
                type: object
                properties:
                  user_id:
                    type: string
                  role:
                    type: string
                  display_name:
                    type: string
                  email:
                    type: string
                  api_keys:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        tier:
                          type: string
                        is_active:
                          type: boolean
                        created_at:
                          type: string
        "401":
          description: Authentication required
    patch:
      operationId: updateProfile
      tags: [Authentication]
      summary: Update user profile
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                display_name:
                  type: string
                phone:
                  type: string
                company_name:
                  type: string
      responses:
        "200":
          description: Updated profile
        "401":
          description: Authentication required

  /auth/rotate-key:
    post:
      operationId: rotateAPIKey
      tags: [Authentication]
      summary: Rotate API key (deactivates old, creates new)
      description: Deactivates the current key and creates a new one with the same tier. Returns the new key once.
      responses:
        "201":
          description: New API key created
          content:
            application/json:
              schema:
                type: object
                properties:
                  api_key:
                    type: string
                    description: New API key (save this — shown once)
                  tier:
                    type: string
                  daily_limit:
                    type: integer
                  old_key_id:
                    type: string
                  note:
                    type: string
        "401":
          description: Authentication required

  /listings/{id}/documents:
    post:
      operationId: submitDocument
      tags: [Documents]
      summary: Submit legal document evidence for a listing
      description: |
        Submit extracted fields from a legal document for cross-verification.
        Upserts on doc_serial match. Returns auto-verification results.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Listing ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DocumentSubmission"
      responses:
        "201":
          description: Document submitted with cross-verification results
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentSubmitResponse"
        "400":
          description: Invalid input
        "404":
          description: Listing not found
    get:
      operationId: getListingDocuments
      tags: [Documents]
      summary: List documents for a listing (tier-gated)
      description: |
        Returns document information for a listing. Response detail depends on API tier:
        - **Partner tier**: Full document list with all fields (owner_name, doc_serial, etc.)
        - **Pro tier**: Verification summary with counts, doc types, and last verified date
        - **Free/Anonymous tier**: Verification summary with status, count, and has_verified_docs flag

        PII fields (owner_name, owner_id) are only available to Partner tier.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Listing ID
      responses:
        "200":
          description: |
            Documents for listing. Partner tier gets full document list.
            Free/Pro tiers get a verification_summary object instead.
          content:
            application/json:
              schema:
                type: object
                properties:
                  listing_id:
                    type: string
                  documents:
                    type: array
                    description: Full document list (partner tier only)
                    items:
                      $ref: "#/components/schemas/Document"
                  total:
                    type: integer
                    description: Total document count (partner tier only)
                  verification_summary:
                    type: object
                    description: Summary for free/pro tiers (no PII)
                    properties:
                      status:
                        type: string
                        enum: [none, pending, partially_verified, verified]
                      doc_count:
                        type: integer
                      has_verified_docs:
                        type: boolean
                      verified_count:
                        type: integer
                        description: Pro tier only
                      pending_count:
                        type: integer
                        description: Pro tier only
                      doc_types:
                        type: array
                        items:
                          type: string
                        description: Pro tier only
                      last_verified_at:
                        type: string
                        format: date-time
                        description: Pro tier only
              examples:
                pro_tier:
                  summary: Pro tier response (verification summary)
                  value:
                    listing_id: "019e..."
                    verification_summary:
                      status: partially_verified
                      doc_count: 2
                      has_verified_docs: true
                      verified_count: 1
                      pending_count: 1
                      doc_types: ["so_hong", "hop_dong"]
                      last_verified_at: "2026-05-20T10:00:00Z"
                free_tier:
                  summary: Free/Anonymous tier response (minimal summary)
                  value:
                    listing_id: "019e..."
                    verification_summary:
                      status: partially_verified
                      doc_count: 2
                      has_verified_docs: true
        "404":
          description: Listing not found

  /meta/gov-land-price:
    get:
      operationId: getGovLandPrice
      tags: [Government Data]
      summary: Look up official government land price by street
      description: |
        Returns official land price records (bảng giá đất) from NQ 87/2025.
        Search by street name (diacritics optional) with optional ward/district filter.
        A street may have multiple sections and land types with different prices.

        **Example:** `GET /meta/gov-land-price?street=Ten+Lua&district=Binh+Tan`
      parameters:
        - name: street
          in: query
          schema:
            type: string
          example: Ten Lua
          description: Street name (Vietnamese, diacritics optional). Required unless ward_code is provided.
        - name: ward
          in: query
          schema:
            type: string
          description: Ward name for disambiguation
        - name: district
          in: query
          schema:
            type: string
          description: District name for disambiguation
        - name: ward_code
          in: query
          schema:
            type: string
          description: Direct ward code lookup (alternative to ward+district)
        - name: land_type
          in: query
          schema:
            type: string
            enum: [ODT, ONT, CLN, LUA, TMD, SXN, NTS, HH]
          description: Filter by land type code
      responses:
        "200":
          description: Matching gov land price records
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GovLandPriceResponse"

  /meta/gov-land-price/ward:
    get:
      operationId: getGovLandPriceByWard
      tags: [Government Data]
      summary: List all government land prices in a ward
      description: |
        Returns all street-level government land prices for a given ward.
        Useful for "what's the price range in this area?" queries.

        **Example:** `GET /meta/gov-land-price/ward?ward=Binh+Tri+Dong&district=Binh+Tan`
      parameters:
        - name: ward_code
          in: query
          schema:
            type: string
          description: Ward code (one of ward_code or ward required)
        - name: ward
          in: query
          schema:
            type: string
          description: Ward name (alternative to ward_code)
        - name: district
          in: query
          schema:
            type: string
          description: District name for disambiguation when using ward name
      responses:
        "200":
          description: All gov land prices for the ward
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GovLandPriceWardResponse"

  /meta/gov-land-price/compare:
    get:
      operationId: compareGovLandPrice
      tags: [Government Data]
      summary: Compare listing price to government land price
      description: |
        Returns the ratio between a listing's asking price per m² and the
        official government land price, with interpretation and legal source.

        The ratio indicates how the asking price compares to the official benchmark:
        - `< 1.5x` — Below typical market range
        - `1.5x–3.0x` — Typical for the area
        - `3.0x–5.0x` — Above average (common in central areas)
        - `> 5.0x` — Significantly above benchmark

        **Note:** Government land price is for tax/compensation purposes — it is NOT market value.
        When no government price is matched, `ratio` and `gov_price_per_m2` are null.
      parameters:
        - name: listing_id
          in: query
          required: true
          schema:
            type: string
          description: Listing ID to compare
      responses:
        "200":
          description: Price comparison with government benchmark
          content:
            application/json:
              schema:
                type: object
                properties:
                  listing_id:
                    type: string
                  listing_price_per_m2:
                    type: integer
                    description: Listing asking price per m² (VND)
                  gov_price_per_m2:
                    type: integer
                    description: Official government land price per m² (VND). Null if no match.
                  ratio:
                    type: number
                    description: listing_price / gov_price. Null if no gov price match.
                  interpretation:
                    type: string
                    description: Human-readable explanation of the ratio
                  gov_price_source:
                    type: object
                    description: Source details for the government price (null if no match)
                    properties:
                      street:
                        type: string
                      ward:
                        type: string
                      district:
                        type: string
                      legal_basis:
                        type: string
                        description: Legal document reference (e.g. NQ 87/2025)
                      cycle:
                        type: string
                        description: Price cycle period (e.g. 2024-2029)
                      effective_date:
                        type: string
                        description: When the price became effective
                  caveat:
                    type: string
              example:
                listing_id: 985fafb9-8503-4e86-9056-d385851708ac
                listing_price_per_m2: 12500000
                gov_price_per_m2: 91800000
                ratio: 0.136
                interpretation: "Listing price is 0.1x official land price (below typical market range)"
                gov_price_source:
                  street: "ĐƯỜNG SỐ 10 (KDC BÌNH HƯNG)"
                  ward: "BÌNH HƯNG"
                  district: "Hồ Chí Minh"
                  legal_basis: "NQ 87/2025/NQ-HĐND TP.HCM"
                  cycle: "2024-2029"
                  effective_date: "2026-01-01"
                caveat: "Government land price is the official benchmark for tax and compensation purposes. It is not market value."
        "400":
          description: Missing listing_id parameter
        "404":
          description: Listing not found

  /meta/gov-land-price/district:
    get:
      operationId: govLandPriceByDistrict
      tags: [Government Data]
      summary: Aggregate government land prices by district
      description: |
        Returns per-district statistics: record count, min/avg/median/max price per m²,
        and available land types. Supports diacritic-insensitive district filtering and land_type filtering.
      parameters:
        - name: district
          in: query
          schema:
            type: string
          description: District name (diacritic-insensitive). Omit for all 22 districts.
        - name: land_type
          in: query
          schema:
            type: string
          description: Filter by land type code (e.g. ODT, CLN)
      responses:
        "200":
          description: District-level price aggregation
          content:
            application/json:
              schema:
                type: object
                properties:
                  districts:
                    type: array
                    items:
                      type: object
                      properties:
                        district:
                          type: string
                        records:
                          type: integer
                        min_price_per_m2:
                          type: integer
                        avg_price_per_m2:
                          type: integer
                        median_price_per_m2:
                          type: integer
                        max_price_per_m2:
                          type: integer
                        land_types:
                          type: array
                          items:
                            type: string

  /meta/risk/check:
    get:
      operationId: checkLocationRisk
      tags: [Government Data]
      summary: Coordinate-based risk assessment
      description: |
        Assesses risk factors for any location in HCMC, independent of a listing.
        Combines 7 factors: zoning classification, DGT overlap, road access, road setback,
        government price availability, ward reform status, and administrative boundary coverage.

        **Risk levels:**
        - `low` — No risk gates triggered, standard due diligence
        - `medium` — Some zoning considerations or missing gov data
        - `high` — Zoning overlap or planning conflict detected
        - `critical` — Major zoning/planning risk, verify with local authority

        Use this to pre-screen locations before visiting properties.
      parameters:
        - name: lat
          in: query
          required: true
          schema:
            type: number
          description: Latitude (WGS84)
        - name: lon
          in: query
          required: true
          schema:
            type: number
          description: Longitude (WGS84)
      responses:
        "200":
          description: Risk assessment with factor breakdown
          content:
            application/json:
              schema:
                type: object
                properties:
                  location:
                    type: object
                    properties:
                      lat:
                        type: number
                      lon:
                        type: number
                  risk_level:
                    type: string
                    enum: [low, medium, high, critical]
                  risk_factors:
                    type: object
                    properties:
                      zoning:
                        type: object
                        description: Zoning risk assessment
                        properties:
                          level:
                            type: string
                            description: Zoning-specific risk level
                          detail:
                            type: string
                            description: Zone code, function, and any DGT overlap
                      gov_price_available:
                        type: boolean
                        description: Whether government land prices exist for this ward
                      ward_reform_status:
                        type: string
                        enum: [pre_reform, post_reform, unknown]
                        description: 2025 ward reform status
                      boundary_coverage:
                        type: boolean
                        description: Whether admin boundaries cover this location
                      dgt_overlap:
                        type: object
                        description: DGT (transport/road) zone overlap
                        properties:
                          has_dgt:
                            type: boolean
                          dgt_pct:
                            type: number
                            description: Percentage of parcel overlapping DGT zone
                      road_access:
                        type: object
                        description: Road access from zoning data
                        properties:
                          accessible:
                            type: boolean
                          detail:
                            type: string
                      road_setback:
                        type: object
                        description: Road setback (lộ giới) information
                        properties:
                          has_setback:
                            type: boolean
                          street:
                            type: string
                          setback_meters:
                            type: number
                  recommendation:
                    type: string
                    description: Action recommendation based on risk level
                  provenance:
                    type: object
                    properties:
                      trust_level:
                        type: string
                      sources:
                        type: array
                        items:
                          type: string
                        description: Data sources used for this assessment
              example:
                location:
                  lat: 10.7228
                  lon: 106.6105
                risk_level: medium
                risk_factors:
                  zoning:
                    level: medium
                    detail: "NNO - Đất nhóm nhà ở hiện trạng kết hợp chỉnh trang, 36% DGT overlap"
                  gov_price_available: true
                  ward_reform_status: post_reform
                  boundary_coverage: true
                  dgt_overlap:
                    has_dgt: true
                    dgt_pct: 36
                  road_access:
                    accessible: true
                    detail: "Road access available"
                  road_setback:
                    has_setback: false
                recommendation: "Moderate risk — some zoning considerations apply. Standard due diligence recommended."
                provenance:
                  trust_level: bds_chat_computed
                  sources:
                    - hcmc_planning_api
                    - nq87_2025
                    - admin_boundary_2025
        "400":
          description: Missing lat/lon parameters

  /meta/zoning:
    get:
      operationId: getZoning
      tags: [Government Data]
      summary: Zoning and road setback lookup by coordinates
      description: |
        Queries official HCMC government planning data for a location.
        Returns zoning classification, road setback lines, and risk assessment.
        Results are cached for 90 days. Source: Sở Quy hoạch Kiến trúc TP.HCM.

        **Example:** `GET /meta/zoning?lat=10.7228&lon=106.6105`
      parameters:
        - name: lat
          in: query
          required: true
          schema:
            type: number
          description: Latitude (WGS84)
        - name: lon
          in: query
          required: true
          schema:
            type: number
          description: Longitude (WGS84)
        - name: street
          in: query
          schema:
            type: string
          description: Street name (for context in response)
        - name: ward
          in: query
          schema:
            type: string
          description: Ward name (for context)
        - name: district
          in: query
          schema:
            type: string
          description: District name (for context)
      responses:
        "200":
          description: Zoning and planning data
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ZoningResponse"

  /meta/zoning/report:
    get:
      operationId: getZoningReport
      tags: [Government Data]
      summary: Generate zoning report card (HTML)
      description: |
        Generates a professional Vietnamese government-style zoning report card (Phiếu Thông Tin Quy Hoạch Sử Dụng Đất) as a self-contained HTML document.
        Includes cadastral diagram, satellite map, zoning classification, road setback, risk assessment, and legal references.
        Optimized for mobile sharing (420px width) via Zalo/Telegram.
        Returns HTML (not JSON). Open in browser or convert to PNG for image sharing.

        **Example:** `GET /meta/zoning/report?lat=10.7228&lon=106.6105`
      parameters:
        - name: lat
          in: query
          required: true
          schema:
            type: number
          description: Latitude (WGS84)
        - name: lon
          in: query
          required: true
          schema:
            type: number
          description: Longitude (WGS84)
      responses:
        "200":
          description: HTML zoning report card
          content:
            text/html:
              schema:
                type: string
        "404":
          description: No zoning data for this location

  /meta/zoning/check-listing:
    get:
      operationId: checkListingZoning
      tags: [Government Data]
      summary: Zoning check for a specific listing
      description: |
        Convenience endpoint: takes a listing ID, uses the listing's coordinates
        to look up zoning. Returns zoning data plus listing context and planning alert.

        **Example:** `GET /meta/zoning/check-listing?listing_id=<listing-uuid>`
      parameters:
        - name: listing_id
          in: query
          required: true
          schema:
            type: string
          description: Listing ID to check
      responses:
        "200":
          description: Zoning data with listing context
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ZoningCheckListingResponse"
        "404":
          description: Listing not found

  /meta/boundaries:
    get:
      operationId: getBoundaries
      tags: [Geography]
      summary: List admin boundaries (districts or wards)
      description: |
        Returns HCMC administrative boundaries at the specified level.
        Use format=geojson to get a GeoJSON FeatureCollection with polygons.
        Default format returns a JSON list with metadata (no geometry).

        **Examples:**
        - Districts: `GET /meta/boundaries?level=district`
        - Wards in a district: `GET /meta/boundaries?level=ward&district_code=769`
        - GeoJSON: `GET /meta/boundaries?level=district&format=geojson`
      parameters:
        - name: level
          in: query
          schema:
            type: string
            enum: [city, district, ward]
            default: district
          description: Administrative level
        - name: format
          in: query
          schema:
            type: string
            enum: [json, geojson]
          description: Response format (default json, geojson includes polygons)
        - name: district_code
          in: query
          schema:
            type: integer
          description: Filter wards by parent district code (only for level=ward)
        - name: reform
          in: query
          schema:
            type: boolean
          description: Filter by reform status (true=post-2025 merged, false=pre-reform original, omit=all)
      responses:
        "200":
          description: List of boundaries or GeoJSON FeatureCollection
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BoundaryListResponse"
            application/geo+json:
              schema:
                type: object
                description: GeoJSON FeatureCollection

  /meta/resolve-ward:
    get:
      operationId: resolveWard
      tags: [Geography]
      summary: Resolve ward name with reform awareness
      description: |
        Resolves a ward name (or lat/lon) to its admin code with full reform context.
        Returns whether the ward is pre-reform or post-reform, what it merged into/from,
        confidence score, and provenance. HCMC merged 312→102 wards effective 2025-07-01.

        **Example:** `GET /meta/resolve-ward?ward=Binh+Tri+Dong&district=Binh+Tan`
      parameters:
        - name: ward
          in: query
          schema:
            type: string
          description: Ward name (Vietnamese, e.g. "Phường 15" or "Bình Tân")
        - name: district
          in: query
          schema:
            type: string
          description: District name for disambiguation
        - name: lat
          in: query
          schema:
            type: number
          description: Latitude (alternative to ward text)
        - name: lon
          in: query
          schema:
            type: number
          description: Longitude (alternative to ward text)
      responses:
        "200":
          description: Ward resolution with reform context
          content:
            application/json:
              schema:
                type: object
                properties:
                  resolution:
                    type: object
                    properties:
                      ward_code:
                        type: string
                      ward_name:
                        type: string
                      district_code:
                        type: string
                      district_name:
                        type: string
                      reform_status:
                        type: string
                        enum: [pre_reform, post_reform, unchanged, unknown]
                      reform_effective:
                        type: string
                      reformed_to:
                        type: object
                        properties:
                          ward_code:
                            type: string
                          ward_name:
                            type: string
                      merged_from:
                        type: array
                        items:
                          type: object
                          properties:
                            ward_code:
                              type: string
                            ward_name:
                              type: string
                      match_method:
                        type: string
                      match_confidence:
                        type: number
                  provenance:
                    type: object
                    properties:
                      trust_level:
                        type: string
                      legal_refs:
                        type: array
                        items:
                          type: string

  /meta/reform-search:
    get:
      operationId: searchReformKnowledge
      tags: [Geography]
      summary: Search HCMC ward reform knowledge base
      description: |
        Full-text search over NQ 1685/NQ-UBTVQH15 reform resolution content.
        Returns relevant chunks (khoản articles + ward summaries) matching the query.
        Useful for answering questions about which wards merged, reform rationale, etc.

        **Example:** `GET /meta/reform-search?q=Binh+Tan&limit=5`
      parameters:
        - name: q
          in: query
          required: true
          schema:
            type: string
          description: Search query (ward name, district, or reform question)
        - name: district
          in: query
          schema:
            type: string
          description: Filter by district name
        - name: limit
          in: query
          schema:
            type: integer
            default: 5
            maximum: 20
          description: Max results to return
      responses:
        "200":
          description: Matching reform knowledge chunks
          content:
            application/json:
              schema:
                type: object
                properties:
                  query:
                    type: string
                  count:
                    type: integer
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        chunk_type:
                          type: string
                          enum: [khoan, ward_summary]
                        khoan_number:
                          type: integer
                        district:
                          type: string
                        ward_names:
                          type: array
                          items:
                            type: string
                        content:
                          type: string
                  provenance:
                    type: object
                    properties:
                      trust_level:
                        type: string
                      legal_refs:
                        type: array
                        items:
                          type: string

  /meta/legal-refs:
    get:
      operationId: getLegalRefs
      tags: [Government Data]
      summary: Browse legal references used by BDS Chat
      description: |
        Returns legal references (laws, resolutions, decrees) that underpin the data in BDS Chat.
        Use to answer questions like "what law governs government land prices?" or "what is NQ 1685?"
        Filter by topic (land_price, ward_reform, zoning, construction), scope (hcm, national), or active status.

        **Examples:**
        - All refs: `GET /meta/legal-refs`
        - By topic: `GET /meta/legal-refs?topic=land_price`
        - Specific: `GET /meta/legal-refs?code=NQ-87-2025`
      parameters:
        - name: topic
          in: query
          schema:
            type: string
            enum: [land_price, ward_reform, zoning, construction, land_law]
          description: Filter by topic area
        - name: scope
          in: query
          schema:
            type: string
            enum: [hcm, national]
          description: Filter by geographic scope
        - name: active
          in: query
          schema:
            type: boolean
          description: Filter by active/expired status (based on effective/expiry dates)
        - name: code
          in: query
          schema:
            type: string
          description: Get a specific legal reference by code (e.g., NQ-1685-2025)
      responses:
        "200":
          description: Legal references
          content:
            application/json:
              schema:
                type: object
                properties:
                  count:
                    type: integer
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        code:
                          type: string
                          description: Reference code (e.g., NQ-1685-2025, NQ-87-2025)
                        title:
                          type: string
                          description: Full title of the legal document
                        title_vi:
                          type: string
                          description: Title in Vietnamese
                        doc_type:
                          type: string
                          description: Document type (nghi_quyet, nghi_dinh, luat, quyet_dinh)
                        topic:
                          type: string
                        scope:
                          type: string
                        effective_date:
                          type: string
                          format: date
                        expiry_date:
                          type: string
                          format: date
                        status:
                          type: string
                          enum: [active, expired, pending]
                        summary:
                          type: string
                          description: Brief summary of the legal document
                        issuing_body:
                          type: string

  /meta/boundaries/contains:
    get:
      operationId: getBoundaryContains
      tags: [Geography]
      summary: Reverse geocode — find boundaries containing a point
      description: |
        Given lat/lon coordinates, returns all admin boundaries (ward, district, city)
        that contain the point. Useful for reverse geocoding a listing's location
        to its administrative hierarchy.

        **Example:** `GET /meta/boundaries/contains?lat=10.7228&lon=106.6105`
      parameters:
        - name: lat
          in: query
          required: true
          schema:
            type: number
          description: Latitude (WGS84)
        - name: lon
          in: query
          required: true
          schema:
            type: number
          description: Longitude (WGS84)
      responses:
        "200":
          description: Matching admin boundaries (ordered ward → district → city)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BoundaryContainsResponse"

  /:
    get:
      operationId: getRoot
      tags: [System]
      security: []
      summary: API info and discovery
      description: Returns API name, docs URL, and health check URL.
      responses:
        "200":
          description: API info
          content:
            application/json:
              schema:
                type: object
                properties:
                  name:
                    type: string
                    example: BDS Chat API
                  docs:
                    type: string
                    format: uri
                    example: https://api.bds.chat/openapi.yaml
                  health:
                    type: string
                    format: uri
                    example: https://api.bds.chat/health
    head:
      operationId: headRoot
      tags: [System]
      security: []
      summary: Root HEAD (health probe)
      description: Returns 200 with no body. Used by uptime monitors.
      responses:
        "200":
          description: OK

  /data-quality:
    get:
      operationId: getDataQuality
      tags: [System]
      security: []
      summary: Public data quality report
      description: |
        Returns per-layer coverage, freshness, consistency checks, and overall quality score across all data assets.
        No authentication required. Supports optional district drill-down via ?district= parameter.
      parameters:
        - name: district
          in: query
          schema:
            type: string
          description: Optional district name for per-district drill-down (e.g., "Quận 1", "Bình Tân")
      responses:
        "200":
          description: Data quality report
          content:
            application/json:
              schema:
                type: object
                properties:
                  generated_at:
                    type: string
                    format: date-time
                  platform:
                    type: string
                  region:
                    type: string
                  layers:
                    type: object
                    description: Per-layer quality stats with source, freshness_slo_days, coverage_pct
                  consistency_checks:
                    type: object
                    description: Layer 3 cross-verification results (X1-X9)
                    properties:
                      all_passed:
                        type: boolean
                      checks:
                        type: object
                  overall_score:
                    type: number
                    description: Weighted aggregate quality score between 0 and 1
                  methodology:
                    type: string
                  improvement_roadmap:
                    type: array
                    description: Top-3 gaps ranked by impact
                    items:
                      type: object
                      properties:
                        priority:
                          type: integer
                        action:
                          type: string
                        impact:
                          type: string

  /reports/data-quality:
    get:
      operationId: getDataQualityReportJSON
      tags: [System]
      security: []
      summary: Data quality report (JSON)
      description: |
        Returns data quality metrics as JSON. No authentication required.
        For the HTML version, use /reports/data-quality.html
      parameters:
        - name: district
          in: query
          schema:
            type: string
          description: Optional district name for per-district drill-down
      responses:
        "200":
          description: Data quality report (JSON)

  /reports/data-quality.html:
    get:
      operationId: getDataQualityReportHTML
      tags: [System]
      security: []
      summary: Data quality report (HTML page)
      description: |
        Professional HTML data quality report with charts, district map, and coverage metrics.
        No authentication required.
      parameters:
        - name: district
          in: query
          schema:
            type: string
          description: Optional district name for per-district drill-down
      responses:
        "200":
          description: HTML report page
          content:
            text/html:
              schema:
                type: string

  /health:
    get:
      operationId: getHealth
      tags: [System]
      security: []
      summary: Health check
      description: Returns {"status":"ok"} when the API is running.
      responses:
        "200":
          description: Healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok

  /openapi.yaml:
    get:
      operationId: getOpenAPISpec
      tags: [System]
      security: []
      summary: OpenAPI specification (this document)
      description: Returns the embedded OpenAPI 3.1 spec as YAML.
      responses:
        "200":
          description: OpenAPI spec
          content:
            text/yaml:
              schema:
                type: string

  /clients/customgpt/openapi.yaml:
    get:
      operationId: getBuyerOpenAPISpec
      tags: [System]
      security: []
      summary: Buyer-focused OpenAPI spec for ChatGPT Custom GPT
      description: Returns a stripped OpenAPI spec with only buyer-facing endpoints (10 of 24).
      responses:
        "200":
          description: Buyer OpenAPI spec
          content:
            text/yaml:
              schema:
                type: string

  /images/{token}:
    get:
      operationId: getImage
      tags: [Images]
      summary: Serve a cached/proxied image (binary)
      description: |
        Returns an image (JPEG/WebP) for the given token. Tokens are SHA256 hashes
        or AES-GCM encrypted source URLs. Automatically converts to WebP when possible.
        Images are cached with immutable Cache-Control headers.
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
          description: Image token (hash or encrypted URL). File extension (.webp, .jpg) is optional and stripped.
        - name: ref
          in: query
          schema:
            type: string
          description: Referrer tag for analytics (chatgpt, direct)
      responses:
        "200":
          description: Image binary
          content:
            image/webp:
              schema:
                type: string
                format: binary
            image/jpeg:
              schema:
                type: string
                format: binary
        "404":
          description: Image not found

  /images:
    post:
      operationId: uploadImage
      tags: [Images]
      summary: Upload an image directly
      description: |
        Upload an image file (max 10MB). Returns a content-addressable ID (SHA256 prefix)
        and a permanent URL. Stored in the upload namespace.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: Image file (JPEG, PNG, WebP, GIF)
              required:
                - file
      responses:
        "201":
          description: Image uploaded
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    description: Content-addressable image ID
                  url:
                    type: string
                    format: uri
                    description: Permanent image URL
                  content_type:
                    type: string
                  size:
                    type: integer
        "400":
          description: Invalid file (not an image or missing)

  /listings/{id}/documents/{doc_id}/images:
    post:
      operationId: uploadDocumentImage
      tags: [Documents]
      summary: Upload a document evidence image
      description: |
        Upload a photo of a legal document (sổ hồng, GPXD, etc.) for evidence.
        Max 10MB per image. Stored in the evidence namespace with listing/doc metadata.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Listing ID
        - name: doc_id
          in: path
          required: true
          schema:
            type: string
          description: Document ID
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
              required:
                - file
      responses:
        "201":
          description: Document image uploaded
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  url:
                    type: string
                    format: uri
                  doc_id:
                    type: string
                  content_type:
                    type: string
        "404":
          description: Document not found

  /admin/documents/review:
    get:
      operationId: getDocumentReviewQueue
      tags: [Admin]
      summary: "[Admin] Document review queue"
      description: |
        Returns documents awaiting human review. Admin endpoint — no auth currently.
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, auto_verified, human_verified, rejected, disputed]
            default: pending
          description: Filter by verification status
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
          description: Max results
      responses:
        "200":
          description: Review queue
          content:
            application/json:
              schema:
                type: object
                properties:
                  documents:
                    type: array
                    items:
                      $ref: "#/components/schemas/Document"
                  total:
                    type: integer
                  status:
                    type: string

  /admin/documents/{doc_id}/verify:
    patch:
      operationId: verifyDocument
      tags: [Admin]
      summary: "[Admin] Verify or reject a document"
      description: |
        Human review decision for a document. Updates verification status and notes.
        Admin endpoint — no auth currently.
      parameters:
        - name: doc_id
          in: path
          required: true
          schema:
            type: string
          description: Document ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VerifyRequest"
      responses:
        "200":
          description: Document updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Document"
        "404":
          description: Document not found

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: API key obtained from POST /auth/register. Format - Bearer bds_live_...
  schemas:
    ListingResponse:
      type: object
      properties:
        total:
          type: integer
          description: Total matching listings
        limit:
          type: integer
        offset:
          type: integer
        metadata:
          $ref: "#/components/schemas/ResponseMetadata"
        freshness_summary:
          type: object
          description: Aggregated freshness stats for returned listings
          properties:
            fresh:
              type: integer
            stale:
              type: integer
            expired:
              type: integer
            slo_days:
              type: integer
        listings:
          type: array
          items:
            $ref: "#/components/schemas/Listing"

    Listing:
      type: object
      properties:
        id:
          type: string
          description: Server-generated UUID v7 (read-only, do not set on create)
        url:
          type: string
          description: Original listing URL
        source:
          type: string
          description: Data source (alonhadat, batdongsan, api)
        title:
          type: string
        price_vnd:
          type: integer
          description: Total price in VND
        price_billion:
          type: number
          description: Total price in tỷ (billions VND)
        price_text:
          type: string
          description: Formatted price string (e.g. "2,2 tỷ")
        area_m2:
          type: number
          description: Land area in square meters
        price_per_m2_vnd:
          type: integer
          description: Price per m² in VND
        price_per_m2_million:
          type: number
          description: Price per m² in triệu (millions VND)
        district:
          type: string
        city:
          type: string
        ward:
          type: string
        street:
          type: string
        latitude:
          type: number
        longitude:
          type: number
        direction:
          type: string
          description: Compass direction (N/S/E/W/NE/SE/NW/SW)
        direction_vi:
          type: string
          description: Direction in Vietnamese (Đông, Tây, Nam, Bắc, Đông Bắc, etc.)
        frontage_m:
          type: number
          description: Street frontage in meters
        road_width_m:
          type: number
          description: Access road width in meters
        legal_status:
          type: string
          description: Legal status code
        legal_status_vi:
          type: string
          description: Legal status in Vietnamese (Sổ hồng riêng, Giấy tay, etc.)
        description:
          type: string
          description: Full listing description in Vietnamese
        images:
          type: array
          items:
            type: string
            format: uri
          description: CDN image URLs (JPEG). URLs are publicly accessible. Note — OpenAI Custom GPT Actions cannot render external images (url_safe restriction). Use source_name + url to link buyers to the original listing for photos.
        image_count:
          type: integer
        contact_name:
          type: string
        contact_phone:
          type: string
        posted_date:
          type: string
          format: date
        expiry_date:
          type: string
          format: date
        listing_type:
          type: string
          description: Property type (dat=land, nha=house, canho=apartment)
        data_quality:
          type: string
          description: Data completeness (full or partial)
        updated_at:
          type: string
          format: date-time
          description: Last data update timestamp

        # LOOP2 fields
        road_facing:
          type: string
          description: Road access type (mat_tien, hem_xe_hoi, hem_xe_may, hem_cut)
        road_facing_vi:
          type: string
          description: Vietnamese label for road_facing (Mặt tiền, Hẻm xe hơi, Hẻm xe máy, Hẻm cụt). Computed, not stored.
        alley_width_m:
          type: number
          description: Narrowest alley width in meters (for hẻm properties)
        land_type:
          type: string
          description: Land use classification (tho_cu, nong_nghiep, hon_hop)
        land_use_area_m2:
          type: number
          description: Residential portion of total area in m² (often less than area_m2)
        structure_type:
          type: string
          description: Building type (dat_nen, nha_cap_4, nha_pho, biet_thu, can_ho, nha_xuong)
        floors:
          type: integer
          description: Number of floors
        bedrooms:
          type: integer
          description: Number of bedrooms
        bathrooms:
          type: integer
          description: Number of bathrooms
        construction_area_m2:
          type: number
          description: Total built area across all floors in m²
        max_floors:
          type: integer
          description: Zoning limit — maximum buildable floors
        seller_type:
          type: string
          description: Seller classification (chinh_chu=direct owner, moi_gioi=broker)
        is_negotiable:
          type: boolean
          description: Whether the price is negotiable
        width_m:
          type: number
          description: Lot width in meters (frontage dimension)
        depth_m:
          type: number
          description: Lot depth in meters
        source_listing_id:
          type: string
          description: Original ID on the source platform. Together with `source`, forms the dedup key (UNIQUE constraint). Auto-generated as content hash for API-created listings if omitted.
        agent_id:
          type: string
          description: Agent who posted the listing (for platform-posted listings)
        first_seen_at:
          type: string
          format: date-time
          description: When this listing was first seen
        price_changed_at:
          type: string
          format: date-time
          description: When the price was last changed
        previous_price_vnd:
          type: integer
          description: Previous price before last change (for price tracking)
        days_on_market:
          type: integer
          description: Days since first seen
        enrichment_source:
          type: string
          description: How the data was enriched (scraped, nlp_extracted, agent_posted, platform_sync)
        enrichment_version:
          type: integer
          description: Enrichment pipeline version

        # Admin ward metadata (derived from GSO reference data)
        admin_ward_code:
          type: string
          description: GSO ward code linking to canonical administrative unit
        price_tier:
          type: string
          description: Ward price tier — central (Q1/Q3/Q5/Q10/Q11/Phú Nhuận), inner_urban (Bình Tân/Gò Vấp/Tân Phú etc.), periurban (Bình Chánh/Hóc Môn/Nhà Bè), satellite (ex-Bình Dương/BRVT)
        legacy_district:
          type: string
          description: Pre-reform (pre-2025) district name for this ward. Useful for price context since Vietnam abolished districts in 2025.
        thu_duc_legacy:
          type: string
          description: For Thủ Đức wards only — ex-Q2, ex-Q9, or ex-TD indicating which legacy district the ward was in. Important because ex-Q2 prices are 3-5x higher than ex-Q9.
        gov_land_price_id:
          type: integer
          description: Internal ID linking to gov land price record
        gov_land_type:
          type: string
          description: Government land classification for price lookup (ODT=residential, TMD=commercial, SXK=industrial)
        gov_price_per_m2:
          type: integer
          description: Government reference price per m² in VND from bảng giá đất (NQ 87/2025). This is the official floor price, not market price.
        market_gov_ratio:
          type: number
          description: Ratio of market price to government price (e.g., 1.3 means market is 30% above official). Central areas typically 1.2-1.5x, periurban 1.0-1.2x.
        quality_score:
          type: number
          description: Data quality score (0.0-1.0). Domain-driven — measures pricing, location, and property completeness. Listings below 0.4 are hidden by default.
        premium_score:
          type: number
          description: Premium characteristic score (0.0-1.0). Measures how desirable the listing is based on location tier, road access, area sweet spot, and market-to-government price ratio. Higher = more premium.
        risk_score:
          type: number
          description: Risk score (0.0-1.0). Measures listing risk based on legal status, seller type, price anomalies, and data quality signals. Higher = riskier.
        risk_band:
          type: string
          enum: [low, medium, high, critical]
          description: Derived from risk_score — low (<0.25), medium (<0.50), high (<0.75), critical (>=0.75). When high or critical, buyer should exercise extra caution.
        trust_grade:
          type: string
          enum: [A, B, C, D, F]
          description: Overall trust grade combining quality_score and risk_score. A = high quality + low risk, F = low quality or high risk. Thresholds — A (quality>=0.7, risk<0.15), B (quality>=0.5, risk<0.3), C (quality>=0.4, risk<0.5), D (quality>=0.25, risk<0.7), F (below D thresholds).

        # LOOP5: Score breakdowns + expert analysis fields
        premium_breakdown:
          type: object
          description: |
            Detailed breakdown of premium_score. Each sub-score measures a specific desirability factor.
            Weights: district(0.30), market_gov_ratio(0.24), area(0.18), road_access(0.14), legal(0.07), structure(0.05), direction(0.02).
          properties:
            weighted_base_score:
              type: number
              description: Weighted sum of all sub-scores before stacking bonus (0-1)
            district_score:
              type: number
              description: District tier score (0-1, weight 0.30). Log-normalized between benchmark 25 tr/m² and 125 tr/m².
            district_benchmark_m2_million:
              type: number
              description: District benchmark price per m² in triệu VND — the reference point for this district.
            road_access_score:
              type: number
              description: Road access quality (0-1, weight 0.14). mat_tien=1.0, hem_xe_hoi=0.6, hem_xe_may=0.35. Frontage width adjusts upward.
            road_access_type:
              type: string
              description: The road_facing value used for scoring
            area_score:
              type: number
              description: Area sweet spot score (0-1, weight 0.18). 50-100m²=1.0, <30m²=0.55, >500m²=0.53.
            area_bucket:
              type: string
              enum: [small, moderate, sweet_spot, large, very_large, oversized]
              description: Area classification — small(<30m²), moderate(30-50m²), sweet_spot(50-100m²), large(100-200m²), very_large(200-500m²), oversized(>500m²)
            legal_score:
              type: number
              description: Legal clarity score (0-1, weight 0.07). SHR/SĐ=0.75, giấy tay=0.0, missing=0.5.
            market_gov_ratio_score:
              type: number
              description: Market-to-government price ratio score (0-1, weight 0.24). Higher ratio = more market demand. <1.0x is suspicious.
            structure_score:
              type: number
              description: Structure quality score (0-1, weight 0.05). Nhà phố/biệt thự boost. Width <3m penalty.
            direction_score:
              type: number
              description: Compass direction score (0-1, weight 0.02). South/SE=0.6 (preferred), West=0.45.
            stacking_bonus:
              type: number
              description: Bonus when 3+ premium factors are high (0-0.12). 3 flags=+0.03, 4+=+0.12.
            premium_flag_count:
              type: integer
              description: Number of premium flags triggered (0-6)
        risk_breakdown:
          type: object
          description: |
            Detailed breakdown of risk_score. Sub-scores identify specific risk factors.
            Hard gates force minimum risk floor for critical conditions (giấy tay → 0.70, gov ratio <0.3x → 0.75, planning keywords → 0.80).
          properties:
            additive_score:
              type: number
              description: Sum of all sub-scores before hard floor (0-1)
            hard_floor:
              type: number
              description: Minimum risk forced by hard gates. Final risk_score = max(additive_score, hard_floor).
            hard_gates_triggered:
              type: array
              items:
                type: string
              description: Hard gate conditions triggered (e.g., "giay_tay_legal", "gov_ratio_below_03x", "planning_keywords")
            subscores:
              type: object
              description: Individual risk sub-scores keyed by factor name (legal, gov_ratio, land_type, price_outlier, text_risk, physical, trust, contradiction, doc_verification)
              additionalProperties:
                type: object
                properties:
                  score:
                    type: number
                    description: Risk contribution (0 to factor max weight)
                  reason:
                    type: string
                    description: What triggered this score
        district_median_price_m2_million:
          type: number
          description: Median price per m² for this district in triệu VND. Computed from listings with quality_score >= 0.40. null if fewer than 5 listings in district.
        price_vs_district_pct:
          type: number
          description: Percentage difference from district median. -14.9 = 15% below average, +20.0 = 20% above. null if district median unavailable.
        source_name:
          type: string
          description: Human-readable source — "Alonhadat", "Batdongsan", or "BDS Chat". Use with url field for source attribution.

        # LOOP9: Planning alert (from zoning cache)
        planning_alert:
          type: object
          description: |
            Zoning and planning data from HCMC government API (cached). Only present when
            listing has coordinates and zoning data is cached. If risk_level is high/critical,
            GPT MUST present the warning prominently.
          properties:
            zone_code:
              type: string
              description: Zoning code (NNO=residential, DGT=transportation, CXDVO=park, etc.)
            zone_function:
              type: string
              description: Zone function description in Vietnamese
            has_road_setback:
              type: boolean
              description: Whether the location is affected by road widening (lộ giới)
            road_setback_meters:
              type: number
              description: Road setback distance in meters
            risk_level:
              type: string
              enum: [none, medium, high, critical, unknown]
              description: Planning risk level. critical=zoned for public use, high=road setback, medium=industrial zone
            warning:
              type: string
              description: Risk explanation in Vietnamese
            source:
              type: string
              description: Data source (Sở Quy hoạch Kiến trúc TP.HCM)

        # LOOP9: Ward reform context
        admin_match_method:
          type: string
          description: How the admin ward was matched (name_normalized, alias, merger_reverse, point_in_polygon)
        admin_match_confidence:
          type: number
          description: Confidence of the admin ward match (0.0-1.0). 1.0=exact post-reform, 0.95=pre-reform, 0.85=alias/inferred

        # LOOP10: International standards
        reso_property_type:
          type: string
          description: RESO Data Dictionary property type (Residential, Land, Commercial, Farm, Industrial). Mapped from Vietnamese land_type via ref_property_types lookup table.
        reso_property_sub_type:
          type: string
          description: RESO property sub-type (SingleFamily, Townhouse, Condominium, Villa, Unimproved, Agricultural, etc.)
        geometry:
          $ref: "#/components/schemas/GeoJSONPoint"
        standard_status:
          type: string
          enum: [Active, Pending, Closed, Expired, Withdrawn, Deleted, "Active Under Contract", "Coming Soon"]
          description: RESO StandardStatus lifecycle field. Default "Active" for new listings. "Closed" replaces legacy "Sold".
        user_id:
          type: string
          description: Owner user ID. Set server-side from authenticated key's user_id. Used for tenant isolation.
        data_class:
          type: string
          enum: [production, scraped, eval]
          description: Data classification. Client writes always get "production". Scraped data gets "scraped". Eval data gets "eval".
        deleted_at:
          type: string
          description: Soft-delete timestamp (ISO 8601). Present only on deleted listings.
        reform_status:
          type: string
          enum: [pre_reform, post_reform]
          description: Ward reform status after NQ 1685/2025. pre_reform=this ward has been merged into another. post_reform=this is the current ward.

        # LOOP9: Provenance
        provenance:
          type: object
          description: Data provenance and trust level for this listing
          properties:
            trust_level:
              type: string
              enum: [government_authoritative, partially_verified, aggregated, user_submitted, bds_chat_computed]
              description: |
                Data trust level:
                - government_authoritative: sourced from official government data
                - partially_verified: has verified legal documents
                - aggregated: scraped from third-party listing sites
                - user_submitted: submitted via API
                - bds_chat_computed: computed/inferred by BDS Chat (lower confidence)
            data_source:
              type: string
              description: Human-readable data source name
            legal_refs:
              type: array
              items:
                type: string
              description: Legal reference codes applicable to this listing (e.g., NQ-1685-2025 for ward reform context)

        # LOOP6: Document verification fields
        doc_count:
          type: integer
          description: Number of legal documents submitted for this listing
        doc_verified:
          type: boolean
          description: Whether at least one document has been verified (auto or human)
        doc_verification_status:
          type: string
          enum: [pending, verified, disputed]
          description: Overall document verification status for this listing
        parcel_id:
          type: string
          description: Land parcel number (thửa đất) from verified document — strongest dedup signal
        map_sheet:
          type: string
          description: Map sheet number (tờ bản đồ) from verified document

        api_key_id:
          type: string
          description: ID of the API key that created this listing (read-only)
        freshness:
          type: object
          description: Freshness status based on SLO monitoring
          properties:
            status:
              type: string
              enum: [fresh, stale, expired, unknown]
            updated_at:
              type: string
              format: date-time
            slo_days:
              type: integer
            days_since_update:
              type: integer

        # LOOP13: Search overlays (opt-in via include= param)
        zoning_overlay:
          type: object
          description: Zoning data overlay (only present when include=zoning). Cached government zoning data for listing coordinates.
          properties:
            zone_code:
              type: string
            zone_function:
              type: string
            risk_level:
              type: string
              enum: [none, medium, high, critical, unknown]
            risk_reason:
              type: string
            dgt_pct:
              type: number
              description: Percentage of DGT (transport) zone overlap
            road_setback_meters:
              type: number
            lbcs_function_code:
              type: string
            lbcs_label:
              type: string
            cached_at:
              type: string
              format: date-time
        gov_price_overlay:
          type: object
          description: Government land price overlay (only present when include=gov_price). Benchmark comparison for listing price vs official land price.
          properties:
            gov_price_per_m2:
              type: integer
            street:
              type: string
            ward:
              type: string
            district:
              type: string
            legal_basis:
              type: string
            cycle:
              type: string
            ratio:
              type: string
              description: Market-to-government price ratio as string (e.g., "3.37")

    DocumentSubmission:
      type: object
      required:
        - doc_type
      properties:
        doc_type:
          type: string
          enum: [so_hong, gpxd, hop_dong, giay_tay, tax_receipt, planning]
          description: |
            Document type:
            - so_hong: Sổ hồng / Sổ đỏ (Land Use Rights Certificate)
            - gpxd: Giấy phép xây dựng (Construction Permit)
            - hop_dong: Hợp đồng mua bán công chứng (Notarized Sale Contract)
            - giay_tay: Giấy tay (Handwritten Deed)
            - tax_receipt: Biên lai thuế đất (Land Tax Receipt)
            - planning: Giấy phép quy hoạch (Planning Certificate)
        doc_serial:
          type: string
          description: Certificate serial number (e.g., "CT 12345678" for sổ hồng). Used for idempotent upsert.
        owner_name:
          type: string
          description: Registered owner name as printed on certificate
        owner_id:
          type: string
          description: Owner's ID number (CMND/CCCD) as printed on certificate
        parcel_id:
          type: string
          description: Thửa đất number from certificate
        map_sheet:
          type: string
          description: Tờ bản đồ number from certificate
        doc_area_m2:
          type: number
          description: Area in m² as stated on certificate
        doc_land_type:
          type: string
          description: |
            Land type as written on certificate. Will be normalized to system codes:
            - ODT/ONT/đất ở → tho_cu
            - CLN/LUA/CHN/NTS/RSX → nong_nghiep
            - TMD/SXK/đất hỗn hợp → hon_hop
        doc_address:
          type: string
          description: Address as printed on certificate
        doc_district:
          type: string
          description: District as printed on certificate
        doc_ward:
          type: string
          description: Ward as printed on certificate
        max_floors:
          type: integer
          description: Maximum floors (from GPXD construction permit)
        use_duration:
          type: string
          description: Land use duration (lau_dai = permanent, or expiry date)
        issued_date:
          type: string
          format: date
          description: Certificate issue date
        issuing_authority:
          type: string
          description: Issuing body (e.g., "UBND TP.HCM")
        qr_data:
          type: string
          description: Raw QR code content if scanned from post-2025 certificate

    DocumentSubmitResponse:
      type: object
      properties:
        id:
          type: string
          description: Document ID
        listing_id:
          type: string
        verification_status:
          type: string
          enum: [auto_verified, pending]
          description: auto_verified if all checks pass + valid serial or QR, otherwise pending for human review
        verification_method:
          type: string
          enum: [field_match, qr_match]
          description: How verification was achieved
        cross_check:
          type: object
          description: Cross-verification results comparing document fields against listing data
          properties:
            area_match:
              type: boolean
              description: Whether doc_area_m2 is within 10% of listing area
            area_delta_pct:
              type: number
              description: Percentage difference between doc and listing area
            land_type_match:
              type: boolean
              description: Whether normalized doc land type matches listing land_type
            address_match:
              type: boolean
              description: Whether doc district+ward matches listing
            owner_matches_seller:
              type: boolean
              description: Whether doc owner name matches listing contact_name
            mismatches:
              type: array
              items:
                type: string
              description: Human-readable descriptions of any mismatches found

    Document:
      type: object
      properties:
        id:
          type: string
        listing_id:
          type: string
        doc_type:
          type: string
          enum: [so_hong, gpxd, hop_dong, giay_tay, tax_receipt, planning]
        doc_serial:
          type: string
        owner_name:
          type: string
        owner_id:
          type: string
        parcel_id:
          type: string
        map_sheet:
          type: string
        doc_area_m2:
          type: number
        doc_land_type:
          type: string
        doc_address:
          type: string
        doc_district:
          type: string
        doc_ward:
          type: string
        max_floors:
          type: integer
        use_duration:
          type: string
        issued_date:
          type: string
          format: date
        issuing_authority:
          type: string
        qr_verified:
          type: boolean
          description: Whether QR code from post-2025 certificate was verified
        doc_images:
          type: array
          items:
            type: string
          description: Image keys for uploaded document photos
        image_count:
          type: integer
        verification_status:
          type: string
          enum: [pending, auto_verified, human_verified, rejected, disputed]
        verification_method:
          type: string
          enum: [field_match, qr_match, human_review]
        verification_notes:
          type: string
        verified_at:
          type: string
          format: date-time
        area_match:
          type: boolean
        land_type_match:
          type: boolean
        address_match:
          type: boolean
        owner_matches_seller:
          type: boolean
        submitted_by:
          type: string
        submitted_via:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    GovLandPriceResponse:
      type: object
      properties:
        query:
          type: object
          properties:
            street:
              type: string
            ward:
              type: string
            district:
              type: string
        results:
          type: array
          items:
            $ref: "#/components/schemas/GovLandPriceRecord"
        count:
          type: integer
        note:
          type: string
          description: Always "Bảng giá đất nhà nước — giá thị trường thường cao hơn 1.2-2.5 lần"

    GovLandPriceWardResponse:
      type: object
      properties:
        ward:
          type: string
        ward_code:
          type: string
        district:
          type: string
        cycle:
          type: string
        streets:
          type: array
          items:
            $ref: "#/components/schemas/GovLandPriceRecord"
        summary:
          type: object
          properties:
            min_price_vnd_per_m2:
              type: integer

            max_price_vnd_per_m2:
              type: integer

            street_count:
              type: integer
        legal_basis:
          type: string

    GovLandPriceRecord:
      type: object
      properties:
        street_name:
          type: string
        ward_code:
          type: string
        ward:
          type: string
        district:
          type: string
        land_type:
          type: string
          description: "ODT=residential, CLN=perennial crop, TMD=commercial, etc."
        land_type_label:
          type: string
          description: Human-readable land type in Vietnamese + English
        price_vnd_per_m2:
          type: integer
          description: Official government price per m² in VND
        section:
          type: string
          description: Street section (Toàn tuyến = entire street, or "Từ X đến Y")
        cycle:
          type: string
          description: Price cycle (e.g., "2024-2029")
        effective_from:
          type: string
          format: date
        effective_to:
          type: string
          format: date
        legal_basis:
          type: string
          description: Legal reference (NQ 87/2025/NQ-HĐND TP.HCM)
        source_url:
          type: string
          format: uri

    ZoningResponse:
      type: object
      properties:
        query:
          type: object
          properties:
            lat:
              type: number
            lon:
              type: number
            street:
              type: string
            ward:
              type: string
            district:
              type: string
        zoning:
          type: object
          properties:
            zone_code:
              type: string
              description: "NNO=residential, DGT=transport, CXDVO=park, CN=industrial, etc."
            zone_function:
              type: string
              description: Zone function in Vietnamese
            zone_function_en:
              type: string
              description: Zone function in English
            block_code:
              type: string
            zone_area_m2:
              type: number
        road_setback:
          type: object
          properties:
            has_setback:
              type: boolean
            street:
              type: string
            setback_meters:
              type: number
            depth_meters:
              type: number
            direction:
              type: string
        planning:
          type: object
          properties:
            parcel_number:
              type: string
            project:
              type: string
            decision:
              type: string
        risk:
          type: object
          properties:
            level:
              type: string
              enum: [none, medium, high, critical, unknown]
            reason:
              type: string
        source:
          type: string
        cached:
          type: boolean
        fetched_at:
          type: string
          format: date-time
        cached_at:
          type: string
          format: date-time
          description: When this zoning data was cached (same as fetched_at)
        cache_age_days:
          type: integer
          description: Number of days since the zoning data was cached

    ZoningCheckListingResponse:
      type: object
      properties:
        listing_id:
          type: string
        listing_street:
          type: string
        listing_ward:
          type: string
        zoning:
          type: object
        road_setback:
          type: object
        risk:
          type: object
          properties:
            level:
              type: string
            reason:
              type: string
        planning_alert:
          type: object
          properties:
            show_warning:
              type: boolean
            warning:
              type: string

    BoundaryListResponse:
      type: object
      properties:
        level:
          type: string
          enum: [city, district, ward]
        count:
          type: integer
        boundaries:
          type: array
          items:
            $ref: "#/components/schemas/AdminBoundary"

    BoundaryContainsResponse:
      type: object
      properties:
        query:
          type: object
          properties:
            lat:
              type: number
            lon:
              type: number
        count:
          type: integer
        boundaries:
          type: array
          items:
            $ref: "#/components/schemas/AdminBoundary"

    AdminBoundary:
      type: object
      properties:
        admin_code:
          type: integer
          description: Administrative unit code (maDVHC)
        name:
          type: string
          description: Name in Vietnamese
        level:
          type: string
          enum: [city, district, ward]
        title:
          type: string
          description: Unit type (Quận, Huyện, Phường, Xã, Thành phố)
        parent_code:
          type: integer
          description: Parent district code (for wards)
        population:
          type: integer
        households:
          type: integer
        area_hectares:
          type: number
        centroid_lat:
          type: number
        centroid_lon:
          type: number
        reform_applied:
          type: boolean
          description: Whether 2025 ward reform merge was applied to this boundary

    ResponseMetadata:
      type: object
      description: Response-level metadata for international standards compliance.
      properties:
        currency:
          type: string
          example: VND
          description: ISO 4217 currency name
        currency_code:
          type: integer
          example: 704
          description: ISO 4217 numeric currency code
        area_unit:
          type: string
          example: SQM
          description: UN/CEFACT area unit code (SQM = square meters)
        crs:
          type: string
          example: "EPSG:4326"
          description: Coordinate Reference System for all geometry in response
        iso_province:
          type: string
          example: VN-SG
          description: ISO 3166-2 province code (VN-SG = Ho Chi Minh City)

    GeoJSONPoint:
      type: object
      description: GeoJSON Point geometry (RFC 7946). Coordinates are [longitude, latitude] in EPSG:4326.
      properties:
        type:
          type: string
          enum: [Point]
        coordinates:
          type: array
          items:
            type: number
          minItems: 2
          maxItems: 2
          description: "[longitude, latitude] in WGS84"

    CreateListingRequest:
      type: object
      description: |
        Request body for creating a listing. Only writable fields are accepted.
        Read-only fields (id, price_billion, price_per_m2_vnd, price_per_m2_million,
        data_quality, geometry, reso_property_type, reso_property_sub_type,
        premium_breakdown, risk_breakdown, provenance, planning_alert, etc.) are
        computed server-side and returned in the response.
      required:
        - title
      properties:
        title:
          type: string
          description: Listing title (required)
        price_vnd:
          type: integer
          description: Total price in VND (1 to 1,000,000,000,000)
        area_m2:
          type: number
          description: Land area in m² (10 to 100,000)
        district:
          type: string
        city:
          type: string
        ward:
          type: string
        street:
          type: string
        latitude:
          type: number
        longitude:
          type: number
        direction:
          type: string
          enum: [N, S, E, W, NE, SE, NW, SW]
        legal_status:
          type: string
          enum: [so_do_so_hong, so_hong, so_hong_rieng, dang_cho_so, giay_tay]
        description:
          type: string
        contact_name:
          type: string
        contact_phone:
          type: string
        posted_date:
          type: string
          format: date
        listing_type:
          type: string
          enum: [dat, nha, canho]
        source:
          type: string
          description: Data source identifier. Defaults to "api" if omitted.
        source_listing_id:
          type: string
          description: Your platform's listing ID. Together with source, forms the dedup key. Auto-generated content hash if omitted.
        agent_id:
          type: string
          description: Agent who posted the listing
        road_facing:
          type: string
          enum: [mat_tien, hem_xe_hoi, hem_xe_may, hem_cut]
        alley_width_m:
          type: number
        land_type:
          type: string
          enum: [tho_cu, nong_nghiep, hon_hop]
        structure_type:
          type: string
          enum: [dat_nen, nha_cap_4, nha_pho, biet_thu, can_ho, nha_xuong]
        floors:
          type: integer
        bedrooms:
          type: integer
        bathrooms:
          type: integer
        construction_area_m2:
          type: number
        max_floors:
          type: integer
        seller_type:
          type: string
          enum: [chinh_chu, moi_gioi]
        is_negotiable:
          type: boolean
        width_m:
          type: number
        depth_m:
          type: number
        frontage_m:
          type: number
        road_width_m:
          type: number
        land_use_area_m2:
          type: number

    ListingInput:
      type: object
      description: |
        Partial update body. Only provided fields are changed — omitted fields keep their current values.
        Same writable fields as CreateListingRequest but nothing is required.
      properties:
        title:
          type: string
        price_vnd:
          type: integer
        area_m2:
          type: number
        district:
          type: string
        ward:
          type: string
        street:
          type: string
        latitude:
          type: number
        longitude:
          type: number
        direction:
          type: string
          enum: [N, S, E, W, NE, SE, NW, SW]
        legal_status:
          type: string
          enum: [so_do_so_hong, so_hong, so_hong_rieng, dang_cho_so, giay_tay]
        description:
          type: string
        contact_name:
          type: string
        contact_phone:
          type: string
        listing_type:
          type: string
          enum: [dat, nha, canho]
        road_facing:
          type: string
          enum: [mat_tien, hem_xe_hoi, hem_xe_may, hem_cut]
        alley_width_m:
          type: number
        land_type:
          type: string
          enum: [tho_cu, nong_nghiep, hon_hop]
        structure_type:
          type: string
          enum: [dat_nen, nha_cap_4, nha_pho, biet_thu, can_ho, nha_xuong]
        floors:
          type: integer
        bedrooms:
          type: integer
        bathrooms:
          type: integer
        seller_type:
          type: string
          enum: [chinh_chu, moi_gioi]
        is_negotiable:
          type: boolean
        width_m:
          type: number
        depth_m:
          type: number
        frontage_m:
          type: number

    VerifyRequest:
      type: object
      description: Admin verification decision for a document.
      required:
        - verification_status
      properties:
        verification_status:
          type: string
          enum: [pending, auto_verified, human_verified, rejected, disputed]
        verification_notes:
          type: string

    TrustReportField:
      type: object
      properties:
        value:
          description: The computed value (string, number, or null if unavailable)
        source:
          type: string
          description: Citation of the data source
        warning:
          type: string
          description: Present when data is missing or unreliable

    TrustReportGovPrice:
      type: object
      properties:
        gov_price_per_m2:
          type: integer
          description: Government reference price in VND per m²
        market_gov_ratio:
          type: number
          description: Market price / gov price ratio
        label:
          type: string
          description: Human-readable ratio assessment
        legal_basis:
          type: string
          description: Legal decree backing the gov price
        street:
          type: string
        cycle:
          type: string
        source:
          type: string
        warning:
          type: string

    TrustReportZoning:
      type: object
      properties:
        zone_code:
          type: string
        zone_function:
          type: string
        risk_level:
          type: string
        warning:
          type: string
        has_road_setback:
          type: boolean
        road_setback_meters:
          type: number
        source:
          type: string

    TrustReportProvenance:
      type: object
      properties:
        trust_level:
          type: string
          enum: [aggregated, user_submitted, bds_chat_computed, partially_verified]
        data_source:
          type: string
        legal_refs:
          type: array
          items:
            type: string

    TrustReportRisk:
      type: object
      properties:
        risk_score:
          type: number
        risk_band:
          type: string
          enum: [low, medium, high, critical]
        hard_gates_triggered:
          type: array
          items:
            type: string
        subscores:
          type: object
          additionalProperties:
            type: object
            properties:
              score:
                type: number
              reason:
                type: string
        source:
          type: string
        warning:
          type: string

    TrustReportDataGap:
      type: object
      properties:
        field:
          type: string
        impact:
          type: string

    TrustReport:
      type: object
      required:
        - listing_id
        - trust_grade
        - quality_score
        - premium_score
        - legal_provenance
        - risk_assessment
        - data_completeness_pct
        - generated_at
      properties:
        listing_id:
          type: string
        trust_grade:
          $ref: "#/components/schemas/TrustReportField"
        quality_score:
          $ref: "#/components/schemas/TrustReportField"
        premium_score:
          $ref: "#/components/schemas/TrustReportField"
        gov_price_comparison:
          $ref: "#/components/schemas/TrustReportGovPrice"
        zoning_compliance:
          $ref: "#/components/schemas/TrustReportZoning"
        legal_provenance:
          $ref: "#/components/schemas/TrustReportProvenance"
        risk_assessment:
          $ref: "#/components/schemas/TrustReportRisk"
        data_completeness_pct:
          type: number
          description: Percentage of key fields present (0-100)
        data_gaps:
          type: array
          items:
            $ref: "#/components/schemas/TrustReportDataGap"
          description: Explicit warnings for missing data fields
        generated_at:
          type: string
          description: ISO 8601 timestamp when the report was generated

    ErrorResponse:
      type: object
      properties:
        error:
          type: string
          description: Error message
