openapi: 3.1.0

# ---------------------------------------------------------------------------
# Shirabe Address API — GPTs Actions 用短縮版 OpenAPI 仕様
#
# 用途: ChatGPT GPT Builder → Actions → Import からの貼り付け/Import URL。
# 本家 https://shirabe.dev/api/v1/address/openapi.yaml は D-1 品質化済(1000+行、
# 日英併記)で、GPT Builder UI の description 長(約300字)制限に抵触する。
# 本ファイルは以下の方針で短縮:
#   - 各 operation.description を 300 字以下
#   - info.description を必要最小限(用途・対象・Free枠・attribution 注意)
#   - x-llm-hint は削除(役割は Instructions 側で担う)
#   - examples を各エンドポイント 1 件に整理
#   - operationId / parameters / response schema は本家と完全同一
# ---------------------------------------------------------------------------

info:
  title: Shirabe Address API
  summary: 日本の住所を ABR 基準で正規化・構造化・座標付与して返す REST API。
  description: |
    Japanese address normalization API backed by the Digital Agency's Address Base Registry (ABR). Returns canonical form, structured components (prefecture/city/town/block/building/floor), postal code, WGS84 coordinates, match level (0-4), and confidence. CC BY 4.0 `attribution` is mandatory on every response and must not be stripped downstream. All 47 Japanese prefectures supported at launch (GA 2026-05-01). Free tier: 5,000 anonymous calls/month. Full spec: https://shirabe.dev/api/v1/address/openapi.yaml
  version: "1.0.0"
  termsOfService: https://shirabe.dev/terms
  contact:
    name: Shirabe (Techwell Inc., Fukuoka, Japan)
    url: https://shirabe.dev
    email: support@shirabe.dev
  license:
    name: Proprietary (API); data CC BY 4.0 (Digital Agency ABR)
    url: https://shirabe.dev/terms

externalDocs:
  description: Full OpenAPI spec and integration guides
  url: https://github.com/techwell-inc-jp/shirabe-address-api

servers:
  - url: https://shirabe.dev
    description: Production

security:
  - ApiKeyAuth: []

tags:
  - name: Address
    description: 住所正規化・ジオコーディング
  - name: Billing
    description: 課金プラン・購入
  - name: System
    description: システム情報

paths:

  /api/v1/address/normalize:
    post:
      tags: [Address]
      summary: Normalize a single Japanese address
      description: |
        Normalize one Japanese address against ABR (47 prefectures). Returns canonical form, components, WGS84 coords, level (0-4), confidence. Ambiguous / not-found / invalid-prefecture cases return HTTP 200 with `result=null` + `error` code. `attribution` (CC BY 4.0) is mandatory.
      operationId: normalizeAddress
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NormalizeRequest'
            example:
              address: "〒106-0032 東京都港区六本木6-10-1 六本木ヒルズ森タワー42F"
      responses:
        "200":
          description: Normalization result (success / ambiguous / not-found / outside-coverage all return 200)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NormalizeResponse'
              example:
                input: "〒106-0032 東京都港区六本木6-10-1 六本木ヒルズ森タワー42F"
                result:
                  normalized: "東京都港区六本木六丁目10番1号"
                  components:
                    prefecture: "東京都"
                    city: "港区"
                    town: "六本木六丁目"
                    block: "10番1号"
                    building: "六本木ヒルズ森タワー"
                    floor: "42F"
                  postal_code: "106-0032"
                  latitude: 35.660491
                  longitude: 139.729223
                  level: 4
                  confidence: 0.98
                candidates: []
                attribution:
                  source: "アドレス・ベース・レジストリ(住所データ)"
                  provider: "デジタル庁"
                  license: "CC BY 4.0"
                  license_url: "https://creativecommons.org/licenses/by/4.0/"
        "400":
          $ref: '#/components/responses/BadRequest'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
        "429":
          $ref: '#/components/responses/RateLimited'
        "500":
          $ref: '#/components/responses/InternalError'
        "503":
          $ref: '#/components/responses/ServiceUnavailable'

  /api/v1/address/normalize/batch:
    post:
      tags: [Address]
      summary: Normalize up to 100 addresses in one call
      description: |
        Batch normalize up to 100 addresses. Per-item results + summary (total/succeeded/ambiguous/failed). HTTP 503 only when every item is SERVICE_UNAVAILABLE; partial failures stay at 200. More than 100 items → BATCH_TOO_LARGE. Every item carries the mandatory `attribution`.
      operationId: batchNormalizeAddresses
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BatchNormalizeRequest'
            example:
              addresses:
                - "東京都港区六本木6-10-1"
                - "大阪府大阪市北区梅田1-1-3"
                - "存在しない住所999"
      responses:
        "200":
          description: Per-item results + summary
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchNormalizeResponse'
              example:
                results:
                  - input: "東京都港区六本木6-10-1"
                    result:
                      normalized: "東京都港区六本木六丁目10番1号"
                      components:
                        prefecture: "東京都"
                        city: "港区"
                        town: "六本木六丁目"
                        block: "10番1号"
                        building: null
                        floor: null
                      postal_code: null
                      latitude: 35.660491
                      longitude: 139.729223
                      level: 4
                      confidence: 0.98
                    candidates: []
                    attribution:
                      source: "アドレス・ベース・レジストリ(住所データ)"
                      provider: "デジタル庁"
                      license: "CC BY 4.0"
                      license_url: "https://creativecommons.org/licenses/by/4.0/"
                  - input: "存在しない住所999"
                    result: null
                    candidates: []
                    error:
                      code: ADDRESS_NOT_FOUND
                      message: "住所を特定できませんでした"
                      matched_up_to: null
                      level: 0
                    attribution:
                      source: "アドレス・ベース・レジストリ(住所データ)"
                      provider: "デジタル庁"
                      license: "CC BY 4.0"
                      license_url: "https://creativecommons.org/licenses/by/4.0/"
                summary:
                  total: 3
                  succeeded: 1
                  ambiguous: 0
                  failed: 2
        "400":
          $ref: '#/components/responses/BadRequest'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          $ref: '#/components/responses/Forbidden'
        "429":
          $ref: '#/components/responses/RateLimited'
        "500":
          $ref: '#/components/responses/InternalError'
        "503":
          $ref: '#/components/responses/ServiceUnavailable'

  /api/v1/address/checkout:
    post:
      tags: [Billing]
      summary: Create a Stripe Checkout Session
      description: |
        Create a Stripe Checkout Session for starter/pro/enterprise signup. No auth. Returns `checkout_url` for browser redirect. A new API key is generated server-side and activated when Stripe confirms payment via webhook. Free plan is not valid — the free tier is anonymous and needs no checkout.
      operationId: createAddressCheckout
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CheckoutRequest'
            example:
              email: "buyer@example.com"
              plan: "starter"
      responses:
        "200":
          description: Checkout URL issued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CheckoutResponse'
              example:
                checkout_url: "https://checkout.stripe.com/c/pay/cs_test_..."
        "400":
          $ref: '#/components/responses/BadRequest'
        "500":
          $ref: '#/components/responses/InternalError'
        "502":
          description: Stripe API call failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: CHECKOUT_FAILED
                  message: "Failed to create checkout session. Please try again."

  /api/v1/address/health:
    get:
      tags: [System]
      summary: Health check
      description: |
        Report API server health and the list of prefectures the active dictionary covers. No auth required. Intended for uptime monitoring and for AI agents to discover supported prefectures at call time.
      operationId: getAddressHealth
      security: []
      responses:
        "200":
          description: Healthy
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
              example:
                status: "ok"
                version: "1.0.0"
                coverage:
                  - "北海道"
                  - "東京都"
                  - "京都府"
                  - "大阪府"
                  - "沖縄県"
                coverage_mode: "nationwide"

components:

  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        `X-API-Key: shrb_<32 alphanumerics>`. Omit the header entirely to use the anonymous Free tier (5,000 calls/month).

  schemas:

    AddressLevel:
      type: integer
      description: |
        Match depth (0-4): 0 = no match, 1 = prefecture, 2 = city, 3 = town, 4 = block / residential number.
      enum: [0, 1, 2, 3, 4]

    Attribution:
      type: object
      description: Mandatory CC BY 4.0 attribution. Must not be stripped from downstream output.
      required: [source, provider, license, license_url]
      properties:
        source:
          type: string
          example: "アドレス・ベース・レジストリ(住所データ)"
        provider:
          type: string
          example: "デジタル庁"
        license:
          type: string
          example: "CC BY 4.0"
        license_url:
          type: string
          format: uri
          example: "https://creativecommons.org/licenses/by/4.0/"

    AddressComponents:
      type: object
      required: [prefecture, city, town, block, building, floor]
      properties:
        prefecture:
          type: [string, "null"]
          example: "東京都"
        city:
          type: [string, "null"]
          description: 市区町村(政令指定都市は区まで含む)/ city (+ ward for 政令指定都市).
          example: "港区"
        town:
          type: [string, "null"]
          example: "六本木六丁目"
        block:
          type: [string, "null"]
          example: "10番1号"
        building:
          type: [string, "null"]
          example: "六本木ヒルズ森タワー"
        floor:
          type: [string, "null"]
          example: "42F"

    NormalizedAddress:
      type: object
      required:
        - normalized
        - components
        - postal_code
        - latitude
        - longitude
        - level
        - confidence
      properties:
        normalized:
          type: string
          example: "東京都港区六本木六丁目10番1号"
        components:
          $ref: '#/components/schemas/AddressComponents'
        postal_code:
          type: [string, "null"]
          example: "106-0032"
        latitude:
          type: [number, "null"]
          example: 35.660491
        longitude:
          type: [number, "null"]
          example: 139.729223
        level:
          $ref: '#/components/schemas/AddressLevel'
        confidence:
          type: number
          minimum: 0
          maximum: 1
          example: 0.98

    AddressError:
      type: object
      required: [code, message, matched_up_to, level]
      properties:
        code:
          $ref: '#/components/schemas/AddressErrorCode'
        message:
          type: string
          example: "市区町村までしか特定できませんでした"
        matched_up_to:
          type: [string, "null"]
          example: "東京都港区"
        level:
          $ref: '#/components/schemas/AddressLevel'

    AddressErrorCode:
      type: string
      description: |
        Error codes. May appear inside a 200-body (`result=null`, `error.code`) or
        inside a 4xx/5xx ErrorResponse. Notable: OUTSIDE_COVERAGE (invalid/typo'd
        prefecture name; all 47 official prefectures are supported), SERVICE_UNAVAILABLE
        (geocoder down), BATCH_TOO_LARGE (>100).
      enum:
        - ADDRESS_NOT_FOUND
        - AMBIGUOUS_INPUT
        - PREFECTURE_NOT_FOUND
        - PARTIAL_MATCH
        - OUTSIDE_COVERAGE
        - INVALID_FORMAT
        - BATCH_TOO_LARGE
        - INVALID_REQUEST
        - INVALID_API_KEY
        - API_KEY_SUSPENDED
        - RATE_LIMIT_EXCEEDED
        - INTERNAL_ERROR
        - CHECKOUT_FAILED
        - SERVICE_UNAVAILABLE

    NormalizeRequest:
      type: object
      required: [address]
      properties:
        address:
          type: string
          minLength: 1
          example: "〒106-0032 東京都港区六本木6-10-1 六本木ヒルズ森タワー42F"

    NormalizeResponse:
      type: object
      required: [input, result, candidates, attribution]
      properties:
        input:
          type: string
          example: "東京都港区六本木6-10-1"
        result:
          oneOf:
            - $ref: '#/components/schemas/NormalizedAddress'
            - type: "null"
        candidates:
          type: array
          items:
            $ref: '#/components/schemas/NormalizedAddress'
        error:
          $ref: '#/components/schemas/AddressError'
        attribution:
          $ref: '#/components/schemas/Attribution'

    BatchNormalizeRequest:
      type: object
      required: [addresses]
      properties:
        addresses:
          type: array
          minItems: 1
          maxItems: 100
          items:
            type: string
            minLength: 1
          example:
            - "東京都港区六本木6-10-1"
            - "大阪府大阪市北区梅田1-1-3"

    BatchNormalizeResponse:
      type: object
      required: [results, summary]
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/NormalizeResponse'
        summary:
          type: object
          required: [total, succeeded, ambiguous, failed]
          properties:
            total:
              type: integer
              minimum: 0
            succeeded:
              type: integer
              minimum: 0
            ambiguous:
              type: integer
              minimum: 0
            failed:
              type: integer
              minimum: 0

    CheckoutRequest:
      type: object
      required: [email, plan]
      properties:
        email:
          type: string
          format: email
          example: "buyer@example.com"
        plan:
          type: string
          enum: [starter, pro, enterprise]
          example: "starter"

    CheckoutResponse:
      type: object
      required: [checkout_url]
      properties:
        checkout_url:
          type: string
          format: uri
          example: "https://checkout.stripe.com/c/pay/cs_test_..."

    HealthResponse:
      type: object
      required: [status, version, coverage, coverage_mode]
      properties:
        status:
          type: string
          enum: [ok, degraded, down]
        version:
          type: string
          example: "1.0.0"
        coverage:
          type: array
          items:
            type: string
          description: All 47 Japanese prefectures.
        coverage_mode:
          type: string
          enum: [nationwide]
          example: "nationwide"

    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              $ref: '#/components/schemas/AddressErrorCode'
            message:
              type: string
              example: "Field 'address' is required and must be a non-empty string"
            details:
              type: object
              additionalProperties: true
            recoveryHint:
              type: string
              example: "Include a non-empty `address` field in the JSON body."

  responses:

    BadRequest:
      description: Invalid request payload.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: INVALID_FORMAT
              message: "Field 'address' is required and must be a non-empty string"
              recoveryHint: "Include `address` (non-empty string) in the JSON body."

    Unauthorized:
      description: Invalid or missing API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: INVALID_API_KEY
              message: "Invalid or missing API key. Include X-API-Key header."

    Forbidden:
      description: API key suspended (payment failure).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: API_KEY_SUSPENDED
              message: "API key suspended due to payment failure."

    RateLimited:
      description: Plan rate limit exceeded.
      headers:
        Retry-After:
          schema:
            type: integer
        X-RateLimit-Limit:
          schema:
            type: integer
        X-RateLimit-Remaining:
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: RATE_LIMIT_EXCEEDED
              message: "Too many requests per second. Please slow down."

    InternalError:
      description: Server-side internal error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: INTERNAL_ERROR
              message: "An unexpected error occurred. Please try again."

    ServiceUnavailable:
      description: |
        Geocoder (Fly.io backend) is temporarily unreachable. Single endpoint always returns 503; batch returns 503 only when every item becomes SERVICE_UNAVAILABLE.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: SERVICE_UNAVAILABLE
              message: "Geocoding service is temporarily unavailable. Please retry in a moment."
