openapi: 3.1.0
info:
  title: RecordsDB API
  version: 1.0.0
  description: |
    Source-specific public-record exports are the canonical RecordsDB API surface.
    Use `/api/v1/sources/{sourceSlug}/search` and `/api/v1/sources/{sourceSlug}/records/{recordId}` for source-backed data products.
    Normalized summary endpoints live under `/api/v1/records/` when they are intentionally cross-source or RecordsDB-curated.
    Raw source captures and operational credentials are never exposed.
servers:
  - url: https://recordsdb.com
    description: Production
  - url: http://127.0.0.1:3205
    description: Local development
tags:
  - name: Sources
  - name: Normalized records
  - name: Legacy
  - name: Public helpers
security:
  - bearerAuth: []
paths:
  /api/v1/sources/la-jefferson-assessor/search:
    get:
      operationId: searchJeffersonAssessorSource
      tags: [Sources]
      summary: Search Jefferson Parish Assessor source records
      description: Returns all available Jefferson Parish Assessor parcel candidates for an address, owner, or parcel query. Address collection searches both real-estate and personal-property modes and merges unique matching parcels. This source export endpoint does not require RecordsDB normalized property linking.
      parameters:
        - $ref: "#/components/parameters/PropertyQuery"
        - name: include
          in: query
          required: false
          description: Use `basic` for core fields, `summary` as a backward-compatible alias for `basic`, `legal` for legal/location fields, `history` for owner and transaction history, or `full` for every available assessor field group.
          schema:
            type: string
            enum: [basic, summary, legal, history, full]
            default: basic
        - name: limit
          in: query
          required: false
          schema: { type: integer, minimum: 1, maximum: 50, default: 20 }
        - name: waitMs
          in: query
          required: false
          description: Milliseconds to wait for a cold source lookup before returning 202 Accepted.
          schema: { type: integer, minimum: 0, maximum: 120000, default: 0 }
      responses:
        "200":
          description: Source search completed
          headers:
            RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/JeffersonAssessorSearchResponse" }
        "202":
          description: Source lookup is still processing; retry after the supplied delay.
          headers:
            Retry-After:
              description: Seconds to wait before retrying the same request.
              schema: { type: integer }
            RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProcessingResponse" }
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "503": { $ref: "#/components/responses/InternalError" }
  /api/v1/sources/la-jefferson-assessor/parcels/{parcelNumber}:
    get:
      operationId: getJeffersonAssessorParcel
      tags: [Legacy]
      summary: Get a Jefferson Parish Assessor source parcel (deprecated)
      deprecated: true
      description: Deprecated compatibility endpoint. New clients should use `/api/v1/sources/la-jefferson-assessor/records/{recordId}` with the parcel number or source record id.
      parameters:
        - name: parcelNumber
          in: path
          required: true
          schema: { type: string, minLength: 1, maxLength: 120 }
        - name: include
          in: query
          required: false
          description: Use `basic`, `legal`, `history`, or `full`.
          schema:
            type: string
            enum: [basic, legal, history, full]
            default: basic
      responses:
        "200":
          description: Source parcel found
          headers:
            Deprecation:
              description: Always `true` for this legacy endpoint.
              schema: { type: string }
            Link:
              description: Successor endpoint link.
              schema: { type: string }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/JeffersonAssessorParcelResponse" }
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /api/v1/sources/la-jefferson-assessor/records/{recordId}:
    get:
      operationId: getJeffersonAssessorSourceRecord
      tags: [Sources]
      summary: Get a Jefferson Parish Assessor source record
      description: Returns a stored Jefferson assessor source record by source record id, parcel number, or source identifier.
      parameters:
        - $ref: "#/components/parameters/SourceRecordId"
        - name: include
          in: query
          required: false
          description: Use `basic`, `legal`, `history`, or `full`.
          schema:
            type: string
            enum: [basic, legal, history, full]
            default: basic
      responses:
        "200":
          description: Source record found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/JeffersonAssessorParcelResponse" }
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /api/v1/sources/la-jefferson-tax/search:
    get:
      operationId: searchJeffersonTaxSource
      tags: [Sources]
      summary: Search Jefferson Parish tax source records
      parameters:
        - $ref: "#/components/parameters/SourceQuery"
        - $ref: "#/components/parameters/SourceLimit"
        - $ref: "#/components/parameters/SourceWaitMs"
      responses:
        "200": { $ref: "#/components/responses/SourceSearchCompleted" }
        "202": { $ref: "#/components/responses/SourceProcessing" }
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "503": { $ref: "#/components/responses/InternalError" }
  /api/v1/sources/la-jefferson-tax/records/{recordId}:
    get:
      operationId: getJeffersonTaxSourceRecord
      tags: [Sources]
      summary: Get a Jefferson Parish tax source record
      parameters:
        - $ref: "#/components/parameters/SourceRecordId"
      responses:
        "200": { $ref: "#/components/responses/SourceRecordFound" }
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /api/v1/sources/la-jefferson-gis/search:
    get:
      operationId: searchJeffersonGisSource
      tags: [Sources]
      summary: Search Jefferson Parish GIS source records
      parameters:
        - $ref: "#/components/parameters/SourceQuery"
        - $ref: "#/components/parameters/SourceLimit"
        - $ref: "#/components/parameters/SourceWaitMs"
      responses:
        "200": { $ref: "#/components/responses/SourceSearchCompleted" }
        "202": { $ref: "#/components/responses/SourceProcessing" }
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "503": { $ref: "#/components/responses/InternalError" }
  /api/v1/sources/la-jefferson-gis/records/{recordId}:
    get:
      operationId: getJeffersonGisSourceRecord
      tags: [Sources]
      summary: Get a Jefferson Parish GIS source record
      parameters:
        - $ref: "#/components/parameters/SourceRecordId"
      responses:
        "200": { $ref: "#/components/responses/SourceRecordFound" }
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /api/v1/sources/la-sos-business/search:
    get:
      operationId: searchLouisianaSosSource
      tags: [Sources]
      summary: Search Louisiana Secretary of State source records
      description: Returns cached parsed Louisiana Secretary of State business records and can queue live SOS-only browser collection when no usable stored match is available.
      parameters:
        - $ref: "#/components/parameters/SourceQuery"
        - $ref: "#/components/parameters/SourceLimit"
        - $ref: "#/components/parameters/SourceWaitMsSos"
      responses:
        "200": { $ref: "#/components/responses/SourceSearchCompleted" }
        "202": { $ref: "#/components/responses/SourceProcessing" }
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "503": { $ref: "#/components/responses/InternalError" }
  /api/v1/sources/la-sos-business/records/{recordId}:
    get:
      operationId: getLouisianaSosSourceRecord
      tags: [Sources]
      summary: Get a stored Louisiana Secretary of State source record
      parameters:
        - $ref: "#/components/parameters/SourceRecordId"
      responses:
        "200": { $ref: "#/components/responses/SourceRecordFound" }
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /api/v1/sources/la-orleans-assessor/search:
    get:
      operationId: searchOrleansAssessorSource
      tags: [Sources]
      summary: Search Orleans Parish Assessor source records
      description: Returns Orleans Parish assessor source records by address, parcel, tax bill, or Beacon assessor key. Stored Orleans index records are returned immediately when present, and RecordsDB queues live Beacon collection to refresh current assessor details for cold or stale matches.
      parameters:
        - $ref: "#/components/parameters/SourceQuery"
        - $ref: "#/components/parameters/SourceLimit"
        - $ref: "#/components/parameters/SourceWaitMs"
      responses:
        "200": { $ref: "#/components/responses/SourceSearchCompleted" }
        "202": { $ref: "#/components/responses/SourceProcessing" }
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "503": { $ref: "#/components/responses/InternalError" }
  /api/v1/sources/la-orleans-assessor/records/{recordId}:
    get:
      operationId: getOrleansAssessorSourceRecord
      tags: [Sources]
      summary: Get an Orleans Parish Assessor source record
      description: Returns a stored Orleans assessor detail or index source record by source record id, parcel, tax bill, or Beacon assessor key.
      parameters:
        - $ref: "#/components/parameters/SourceRecordId"
      responses:
        "200": { $ref: "#/components/responses/SourceRecordFound" }
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /api/v1/records/entities/search:
    get:
      operationId: searchBusinessEntitySummaries
      tags: [Normalized records]
      summary: Search normalized Louisiana business entity summaries
      description: Returns customer-facing Louisiana Secretary of State business entity summaries from stored source records. Use `/api/v1/sources/la-sos-business/search` when source-record fields and optional live collection wait behavior are needed.
      parameters:
        - $ref: "#/components/parameters/SourceQuery"
      responses:
        "200":
          description: Entity search completed
          headers:
            RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BusinessEntitySearchResponse" }
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /api/v1/entities/search:
    get:
      operationId: searchBusinessEntitiesLegacy
      tags: [Legacy]
      summary: Search Louisiana business entity summaries (deprecated)
      deprecated: true
      description: Deprecated compatibility endpoint. New clients should use `/api/v1/sources/la-sos-business/search` for source-backed SOS data or `/api/v1/records/entities/search` for normalized RecordsDB entity summaries.
      parameters:
        - $ref: "#/components/parameters/SourceQuery"
      responses:
        "200":
          description: Entity search completed
          headers:
            Deprecation:
              description: Always `true` for this legacy endpoint.
              schema: { type: string }
            Link:
              description: Successor and alternate endpoint links.
              schema: { type: string }
            RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BusinessEntitySearchResponse" }
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /api/v1/properties/suggest:
    get:
      operationId: suggestProperties
      tags: [Public helpers]
      summary: Suggest property records
      security: []
      parameters:
        - name: q
          in: query
          required: false
          schema: { type: string, maxLength: 200 }
      responses:
        "200":
          description: Suggestions returned
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/PropertySuggestion" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /api/address-search:
    get:
      operationId: suggestAddresses
      tags: [Public helpers]
      summary: Suggest Louisiana addresses through Nominatim
      security: []
      parameters:
        - name: q
          in: query
          required: false
          schema: { type: string, maxLength: 120 }
      responses:
        "200":
          description: Address suggestions or an empty list
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/AddressSuggestion" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /api/public-property-lookup/status:
    get:
      operationId: publicPropertyLookupStatus
      tags: [Public helpers]
      summary: Check whether a public property report is ready
      security: []
      parameters:
        - name: q
          in: query
          required: true
          schema: { type: string, minLength: 1, maxLength: 160 }
      responses:
        "200":
          description: Public property report status returned
          headers:
            RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PublicPropertyLookupStatus" }
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: RecordsDB API key
      description: API key beginning with rdb_live_.
  parameters:
    PropertyQuery:
      name: q
      in: query
      required: true
      schema: { type: string, minLength: 1, maxLength: 200 }
      example: 2400 Veterans Blvd, Kenner, LA 70062
    SourceQuery:
      name: q
      in: query
      required: true
      schema: { type: string, minLength: 1, maxLength: 200 }
    SourceLimit:
      name: limit
      in: query
      required: false
      schema: { type: integer, minimum: 1, maximum: 50, default: 20 }
    SourceWaitMs:
      name: waitMs
      in: query
      required: false
      schema: { type: integer, minimum: 0, maximum: 55000, default: 0 }
    SourceWaitMsSos:
      name: waitMs
      in: query
      required: false
      description: Milliseconds to wait for a cold Louisiana SOS source collection before returning 202 Accepted.
      schema: { type: integer, minimum: 0, maximum: 120000, default: 0 }
    SourceRecordId:
      name: recordId
      in: path
      required: true
      schema: { type: string, minLength: 1, maxLength: 200 }
  headers:
    RateLimitLimit:
      description: Maximum requests in the active one-minute window.
      schema: { type: integer }
    RateLimitRemaining:
      description: Requests remaining in the active window.
      schema: { type: integer }
    RateLimitReset:
      description: Unix timestamp when the active window resets.
      schema: { type: integer }
  responses:
    SourceSearchCompleted:
      description: Source search completed
      content:
        application/json:
          schema: { $ref: "#/components/schemas/GenericSourceSearchResponse" }
    SourceRecordFound:
      description: Source record found
      content:
        application/json:
          schema: { $ref: "#/components/schemas/GenericSourceRecordResponse" }
    SourceProcessing:
      description: Source collection is still processing.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ProcessingResponse" }
    InvalidRequest:
      description: Request validation failed; no lookup unit is charged.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiErrorResponse" }
          example:
            error:
              code: INVALID_REQUEST
              message: Query parameter q is required
              details: { parameter: q }
    Unauthorized:
      description: API key is missing or invalid; no lookup unit is charged.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiErrorResponse" }
          examples:
            missing:
              value: { error: { code: MISSING_API_KEY, message: API key is required } }
            invalid:
              value: { error: { code: INVALID_API_KEY, message: Invalid API key } }
    NotFound:
      description: Lookup completed but no record was found; one lookup unit is charged.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiErrorResponse" }
          example: { error: { code: NOT_FOUND, message: Property record not found } }
    TooManyRequests:
      description: Rate limit or lookup balance exceeded; no lookup unit is charged.
      headers:
        RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
        RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
        RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiErrorResponse" }
          examples:
            rate:
              value: { error: { code: RATE_LIMITED, message: Rate limit exceeded } }
            quota:
              value: { error: { code: QUOTA_EXCEEDED, message: API lookup balance used } }
    InternalError:
      description: Source lookup failed or source is temporarily unavailable.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiErrorResponse" }
          example: { error: { code: INTERNAL_ERROR, message: Jefferson Parish Assessor lookup failed. } }
  schemas:
    NullableString:
      type: [string, "null"]
    ApiErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum: [INVALID_REQUEST, MISSING_API_KEY, INVALID_API_KEY, QUOTA_EXCEEDED, NOT_FOUND, RATE_LIMITED, INTERNAL_ERROR]
            message: { type: string }
            details:
              type: object
              additionalProperties: true
    ProcessingResponse:
      type: object
      required: [status, message, data]
      properties:
        status: { type: string, enum: [processing] }
        message: { type: string }
        data:
          type: array
          maxItems: 0
          items: {}
    GenericSourceSearchResponse:
      type: object
      required: [source, data]
      properties:
        source: { type: string }
        data:
          type: array
          items:
            type: object
            additionalProperties: true
    GenericSourceRecordResponse:
      type: object
      required: [source, data]
      properties:
        source: { type: string }
        data:
          type: object
          additionalProperties: true
    JeffersonAssessorSearchResponse:
      type: object
      required: [source, data]
      properties:
        source: { type: string }
        data:
          type: array
          items: { $ref: "#/components/schemas/JeffersonAssessorRecord" }
    BusinessEntitySearchResponse:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/BusinessEntitySummary" }
    BusinessEntitySummary:
      type: object
      properties:
        name: { $ref: "#/components/schemas/NullableString" }
        entityType: { $ref: "#/components/schemas/NullableString" }
        status: { $ref: "#/components/schemas/NullableString" }
        registeredAgent: { $ref: "#/components/schemas/NullableString" }
        domicileAddress: { $ref: "#/components/schemas/NullableString" }
        mailingAddress: { $ref: "#/components/schemas/NullableString" }
        charterNumber: { $ref: "#/components/schemas/NullableString" }
        annualReportStatus: { $ref: "#/components/schemas/NullableString" }
        registrationDate: { $ref: "#/components/schemas/NullableString" }
        fileDate: { $ref: "#/components/schemas/NullableString" }
        lastReportFiled: { $ref: "#/components/schemas/NullableString" }
        previousNames:
          type: array
          items: {}
        registeredAgents:
          type: array
          items: {}
        officers:
          type: array
          items: {}
        affiliations:
          type: array
          items: {}
        amendments:
          type: array
          items: {}
        sourceRecordUrl: { $ref: "#/components/schemas/NullableString" }
        source: { type: string }
    JeffersonAssessorParcelResponse:
      type: object
      required: [source, data]
      properties:
        source: { type: string }
        data:
          $ref: "#/components/schemas/JeffersonAssessorRecord"
    JeffersonAssessorRecord:
      type: object
      required: [source, recordType, recordKind, parcelNumber, ownerName, physicalAddress, sourceRecordUrl]
      properties:
        source: { type: string }
        recordType: { type: string, enum: [assessor_parcel] }
        recordKind: { type: string, enum: [real_estate, business_personal_property, unknown] }
        parcelNumber: { $ref: "#/components/schemas/NullableString" }
        ownerName: { $ref: "#/components/schemas/NullableString" }
        mailingAddress: { $ref: "#/components/schemas/NullableString" }
        physicalAddress: { $ref: "#/components/schemas/NullableString" }
        legalDescription: { $ref: "#/components/schemas/NullableString" }
        taxYear: { type: [integer, "null"] }
        ward: { $ref: "#/components/schemas/NullableString" }
        parcelType: { $ref: "#/components/schemas/NullableString" }
        propertyClass: { $ref: "#/components/schemas/NullableString" }
        assessedValue: { type: [number, "null"] }
        marketValue: { type: [number, "null"] }
        sourceRecordUrl: { $ref: "#/components/schemas/NullableString" }
        location:
          type: [object, "null"]
          properties:
            subdivision: { $ref: "#/components/schemas/NullableString" }
            block: { $ref: "#/components/schemas/NullableString" }
            lot: { $ref: "#/components/schemas/NullableString" }
            section: { $ref: "#/components/schemas/NullableString" }
            township: { $ref: "#/components/schemas/NullableString" }
            range: { $ref: "#/components/schemas/NullableString" }
            tract: { $ref: "#/components/schemas/NullableString" }
        assessmentLines:
          type: array
          items:
            type: object
            properties:
              propertyClass: { $ref: "#/components/schemas/NullableString" }
              assessedValue: { type: [number, "null"] }
              marketValue: { type: [number, "null"] }
              units: { type: [number, "null"] }
        ownerHistory:
          type: array
          items:
            type: object
            properties:
              name: { $ref: "#/components/schemas/NullableString" }
              address: { $ref: "#/components/schemas/NullableString" }
              homestead: { $ref: "#/components/schemas/NullableString" }
              primary: { $ref: "#/components/schemas/NullableString" }
              ownershipPercent: { type: [number, "null"] }
              taxPercent: { type: [number, "null"] }
              fromDate: { $ref: "#/components/schemas/NullableString" }
              toDate: { $ref: "#/components/schemas/NullableString" }
        transactions:
          type: array
          items:
            type: object
            properties:
              deedNumber: { $ref: "#/components/schemas/NullableString" }
              instrumentType: { $ref: "#/components/schemas/NullableString" }
              soldAt: { $ref: "#/components/schemas/NullableString" }
              amount: { type: [number, "null"] }
              bookAndPage: { $ref: "#/components/schemas/NullableString" }
        officialUrls:
          type: object
          properties:
            assessor: { $ref: "#/components/schemas/NullableString" }
            map: { $ref: "#/components/schemas/NullableString" }
    PropertySuggestion:
      type: object
      required: [label, value, subtitle, owner, parcelNumber, street, city, state, zip]
      properties:
        label: { type: string }
        value: { type: string }
        subtitle: { type: string }
        owner: { $ref: "#/components/schemas/NullableString" }
        parcelNumber: { $ref: "#/components/schemas/NullableString" }
        street: { type: string }
        city: { $ref: "#/components/schemas/NullableString" }
        state: { type: string }
        zip: { $ref: "#/components/schemas/NullableString" }
    AddressSuggestion:
      type: object
      required: [id, label, value, street, city, state, zip, source]
      properties:
        id: { type: string }
        label: { type: string }
        value: { type: string }
        street: { type: string }
        city: { type: string }
        state: { type: string }
        zip: { type: string }
        source: { type: string, const: openstreetmap }
    PublicPropertyLookupStatus:
      oneOf:
        - type: object
          required: [status, path]
          properties:
            status: { type: string, const: ready }
            path: { type: string }
        - type: object
          required: [status]
          properties:
            status: { type: string, const: pending }
