{
 "openapi": "3.1.0",
 "info": {
  "title": "มดแดง Mot Dang — Chiang Mai & Chiang Rai city directory",
  "version": "1.0.0",
  "summary": "A Thai-first open directory, queryable. No key, no sign-up.",
  "description": "Every place the site publishes, filterable by category, tag, facet, completeness, proximity and opening hours.\n\nTake the data. The only thing asked in return is attribution to มดแดง Mot Dang (motdang.net). Full terms: https://motdang.net/terms.html",
  "license": {
   "name": "CC BY 4.0 (compilation); ODbL 1.0 on OSM-derived rows",
   "url": "https://motdang.net/terms.html"
  },
  "contact": {
   "url": "https://motdang.net/api/"
  }
 },
 "servers": [
  {
   "url": "https://motdang.net/api/v1"
  }
 ],
 "paths": {
  "/": {
   "get": {
    "summary": "What this API is, and every endpoint in it",
    "responses": {
     "200": {
      "description": "Service descriptor"
     }
    }
   }
  },
  "/places": {
   "get": {
    "summary": "Query places",
    "parameters": [
     {
      "name": "q",
      "in": "query",
      "required": false,
      "description": "Substring match over name, other names, address, category and tag words, in Thai or English. Space-separated terms are ANDed. For meaning rather than letters use the semantic endpoint named in the descriptor.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "province",
      "in": "query",
      "required": false,
      "description": "cm | cr",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "cat",
      "in": "query",
      "required": false,
      "description": "Comma-separated category keys; a place matching ANY of them.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "sub",
      "in": "query",
      "required": false,
      "description": "Comma-separated subcategory keys; ANY.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "tag",
      "in": "query",
      "required": false,
      "description": "Comma-separated tag slugs; ANY. See /api/v1/tags.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "lens",
      "in": "query",
      "required": false,
      "description": "Comma-separated lens keys; ANY.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "facet",
      "in": "query",
      "required": false,
      "description": "Comma-separated facet keys; ALL — each is a requirement.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "has",
      "in": "query",
      "required": false,
      "description": "Comma-separated from: nameTh, nameEn, phone, line, hours, web, photo, claimed, fresh; ALL.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "min_ants",
      "in": "query",
      "required": false,
      "description": "0–9. The ant rank is nine completeness checks, not a quality score.",
      "schema": {
       "type": "integer"
      }
     },
     {
      "name": "near",
      "in": "query",
      "required": false,
      "description": "lat,lng. Places with no pin cannot answer this and drop out of the result rather than being placed.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "radius",
      "in": "query",
      "required": false,
      "description": "Metres from near=. Default 2000, max 50000.",
      "schema": {
       "type": "integer"
      }
     },
     {
      "name": "bbox",
      "in": "query",
      "required": false,
      "description": "west,south,east,north in degrees — the viewport as a question. Pinned rows inside the box; rows with no pin leave the answer.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "open_now",
      "in": "query",
      "required": false,
      "description": "Restrict to places open at this moment in Asia/Bangkok. Unknown hours are EXCLUDED, not called closed.",
      "schema": {
       "type": "boolean"
      }
     },
     {
      "name": "open_at",
      "in": "query",
      "required": false,
      "description": "A time instead of now, e.g. 'Sa 18:30'. Wins over open_now when both are given.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "sort",
      "in": "query",
      "required": false,
      "description": "distance | ants | name | relevance. Defaults to distance with near=, relevance with q=, else ants.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "description": "Default 20, max 200. Asking for more gets the cap, not an error.",
      "schema": {
       "type": "integer"
      }
     },
     {
      "name": "offset",
      "in": "query",
      "required": false,
      "description": "For paging. total says how many matched.",
      "schema": {
       "type": "integer"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "A page of results",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "schemaVersion": {
           "type": "integer"
          },
          "generated": {
           "type": "string"
          },
          "query": {
           "type": "object"
          },
          "total": {
           "type": "integer"
          },
          "count": {
           "type": "integer"
          },
          "limit": {
           "type": "integer"
          },
          "offset": {
           "type": "integer"
          },
          "results": {
           "type": "array",
           "items": {
            "type": "object",
            "properties": {
             "id": {},
             "url": {},
             "apiUrl": {},
             "province": {},
             "name": {},
             "nameEn": {},
             "categories": {},
             "subcategories": {},
             "lat": {},
             "lng": {},
             "geoPrecision": {},
             "hours": {},
             "openNow": {},
             "antRank": {},
             "has": {},
             "tags": {},
             "lenses": {},
             "facets": {},
             "confidence": {},
             "osmDerived": {},
             "updatedAt": {},
             "photoUrl": {},
             "distanceM": {}
            }
           }
          },
          "attribution": {
           "type": "string"
          },
          "licence": {
           "type": "object"
          },
          "terms": {
           "type": "string"
          }
         }
        }
       }
      }
     }
    }
   }
  },
  "/places/{idOrSlug}": {
   "get": {
    "summary": "One place, every field",
    "description": "The same object the place page serves at its own URL with .json — one generator, so the API and the page cannot disagree.",
    "parameters": [
     {
      "name": "idOrSlug",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "The stable id or the page slug. The id is unique; a page slug can be shared by two records, and a shared one is answered with 300 and the candidate ids rather than a guess."
     }
    ],
    "responses": {
     "200": {
      "description": "The full record"
     },
     "300": {
      "description": "The page slug is shared by more than one record; `candidates` lists them by id. Ask again by id."
     },
     "404": {
      "description": "No such place"
     }
    }
   }
  },
  "/search": {
   "get": {
    "summary": "Places ranked by words, as the site's search box ranks them",
    "parameters": [
     {
      "name": "q",
      "in": "query",
      "required": false,
      "description": "Words in Thai or English. A city named here narrows to its province.",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "province",
      "in": "query",
      "required": false,
      "description": "cm | cr",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "description": "Default 20, max 200.",
      "schema": {
       "type": "integer"
      }
     },
     {
      "name": "offset",
      "in": "query",
      "required": false,
      "description": "For paging. total says how many matched.",
      "schema": {
       "type": "integer"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "A page of ranked results",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "schemaVersion": {
           "type": "integer"
          },
          "generated": {
           "type": "string"
          },
          "engine": {
           "type": "string"
          },
          "query": {
           "type": "object"
          },
          "total": {
           "type": "integer"
          },
          "count": {
           "type": "integer"
          },
          "tier": {
           "type": [
            "string",
            "null"
           ],
           "description": "exact | thesaurus | loose | partial, of the first result"
          },
          "notes": {
           "type": "array",
           "items": {
            "type": "string"
           },
           "description": "How q was read: mended, segmented, stopwords, widened, intent"
          },
          "results": {
           "type": "array",
           "items": {
            "type": "object",
            "properties": {
             "id": {},
             "url": {},
             "apiUrl": {},
             "province": {},
             "name": {},
             "nameEn": {},
             "categories": {},
             "subcategories": {},
             "lat": {},
             "lng": {},
             "geoPrecision": {},
             "hours": {},
             "openNow": {},
             "antRank": {},
             "has": {},
             "tags": {},
             "lenses": {},
             "facets": {},
             "confidence": {},
             "osmDerived": {},
             "updatedAt": {},
             "photoUrl": {},
             "distanceM": {},
             "score": {
              "type": "number"
             },
             "tier": {
              "type": "string"
             }
            }
           }
          },
          "attribution": {
           "type": "string"
          },
          "licence": {
           "type": "object"
          },
          "terms": {
           "type": "string"
          }
         }
        }
       }
      }
     },
     "500": {
      "description": "The engine broke on this query; /places?q= answers"
     },
     "503": {
      "description": "The search shards are not published; /places?q= answers"
     }
    }
   }
  },
  "/categories": {
   "get": {
    "summary": "The category tree with live counts",
    "responses": {
     "200": {
      "description": "Categories"
     }
    }
   }
  },
  "/tags": {
   "get": {
    "summary": "Every tag, lens and facet key in use",
    "responses": {
     "200": {
      "description": "Vocabularies"
     }
    }
   }
  },
  "/health": {
   "get": {
    "summary": "Is the index loaded, and how old is it",
    "responses": {
     "200": {
      "description": "OK"
     },
     "503": {
      "description": "Index unavailable"
     }
    }
   }
  },
  "/index.json": {
   "get": {
    "summary": "The whole query index in one file",
    "description": "Take this instead of paging if you want more than a few hundred rows. It is the same data the endpoints answer from.",
    "responses": {
     "200": {
      "description": "Bulk index"
     }
    }
   }
  }
 }
}