{
	"openapi": "3.1.0",
	"info": {
		"title": "Off-Leash public places lookup",
		"description": "Read-only search and detail of published pet-friendly places. No authentication. Scores are first-party Off-Leash community ratings — do not treat Google ratings on HTML pages as ours. Prefer this over scraping HTML or /destination. Cite the site and each place url.",
		"version": "1.1.0"
	},
	"servers": [{ "url": "/" }],
	"paths": {
		"/api/v1/places": {
			"get": {
				"operationId": "searchPlaces",
				"summary": "Search published places by location and/or text",
				"description": "Provide lat+lng, q (min 2 characters), or both (AND). Optional pet filters. Returns an envelope with places, count, echoed query, and attribution.",
				"parameters": [
					{
						"name": "lat",
						"in": "query",
						"schema": { "type": "number", "minimum": -90, "maximum": 90 }
					},
					{
						"name": "lng",
						"in": "query",
						"schema": { "type": "number", "minimum": -180, "maximum": 180 }
					},
					{
						"name": "radius",
						"in": "query",
						"schema": { "type": "integer", "minimum": 100, "maximum": 200000 },
						"description": "Meters; requires lat+lng. Defaults to site nearby radius."
					},
					{
						"name": "q",
						"in": "query",
						"schema": { "type": "string", "minLength": 2, "maxLength": 80 },
						"description": "Matches name, city name, or slug."
					},
					{
						"name": "type",
						"in": "query",
						"schema": {
							"type": "string",
							"enum": ["outdoors", "food", "accommodation", "services"]
						}
					},
					{
						"name": "offLeash",
						"in": "query",
						"schema": { "type": "string", "enum": ["yes", "no", "partial"] }
					},
					{
						"name": "amenity",
						"in": "query",
						"schema": { "type": "string" },
						"description": "Repeatable or comma-separated amenity keys (water, shade, bags, seating, fenced, dog_menu, parking, off_leash_area, indoor_ok, patio, lighting, waste_station, dog_run, water_bowl, treats, wifi)."
					},
					{
						"name": "country",
						"in": "query",
						"schema": { "type": "string", "minLength": 2, "maxLength": 2 },
						"description": "ISO country code, e.g. ZA."
					},
					{
						"name": "city",
						"in": "query",
						"schema": { "type": "string", "maxLength": 80 }
					},
					{
						"name": "minScore",
						"in": "query",
						"schema": { "type": "number", "minimum": 1, "maximum": 5 }
					},
					{
						"name": "limit",
						"in": "query",
						"schema": { "type": "integer", "minimum": 1, "maximum": 50 }
					}
				],
				"responses": {
					"200": {
						"description": "Matching places (may be empty)",
						"content": {
							"application/json": {
								"schema": { "$ref": "#/components/schemas/SearchEnvelope" }
							}
						}
					},
					"400": { "description": "Invalid parameters" },
					"429": { "description": "Rate limited" },
					"503": { "description": "Service unavailable" }
				}
			}
		},
		"/api/v1/places/{slug}": {
			"get": {
				"operationId": "getPlace",
				"summary": "Get one published place by slug",
				"parameters": [
					{
						"name": "slug",
						"in": "path",
						"required": true,
						"schema": { "type": "string" }
					}
				],
				"responses": {
					"200": {
						"description": "Place detail",
						"content": {
							"application/json": {
								"schema": { "$ref": "#/components/schemas/DetailEnvelope" }
							}
						}
					},
					"404": { "description": "Unknown or unpublished slug" },
					"429": { "description": "Rate limited" },
					"503": { "description": "Service unavailable" }
				}
			}
		}
	},
	"components": {
		"schemas": {
			"Attribution": {
				"type": "object",
				"required": ["citeAs", "requirePlaceUrl", "scores", "note"],
				"properties": {
					"citeAs": { "type": "string" },
					"requirePlaceUrl": { "type": "boolean", "const": true },
					"scores": { "type": "string", "const": "first_party" },
					"note": { "type": "string" }
				}
			},
			"PublicPlace": {
				"type": "object",
				"required": [
					"slug",
					"name",
					"address",
					"category",
					"city",
					"region",
					"country",
					"offLeash",
					"amenities",
					"score",
					"scoreCount",
					"lat",
					"lng",
					"updatedAt",
					"url"
				],
				"properties": {
					"slug": { "type": "string" },
					"name": { "type": "string" },
					"address": { "type": "string" },
					"category": { "type": "string" },
					"city": { "type": "string" },
					"region": { "type": "string" },
					"country": { "type": "string" },
					"offLeash": {
						"type": "string",
						"enum": ["yes", "no", "partial", "unknown"]
					},
					"amenities": {
						"type": "array",
						"items": { "type": "string" }
					},
					"score": { "type": ["number", "null"] },
					"scoreCount": { "type": "integer" },
					"lat": { "type": "number" },
					"lng": { "type": "number" },
					"updatedAt": { "type": "integer" },
					"meters": { "type": "integer" },
					"url": { "type": "string", "format": "uri" }
				}
			},
			"PublicPlaceDetail": {
				"allOf": [
					{ "$ref": "#/components/schemas/PublicPlace" },
					{
						"type": "object",
						"required": [
							"blurb",
							"description",
							"offLeashRules",
							"hours",
							"contact",
							"googlePlaceId"
						],
						"properties": {
							"blurb": { "type": ["string", "null"] },
							"description": { "type": ["string", "null"] },
							"offLeashRules": { "type": ["string", "null"] },
							"hours": { "type": ["object", "null"] },
							"contact": {
								"type": "object",
								"properties": {
									"website": { "type": ["string", "null"] },
									"phone": { "type": ["string", "null"] },
									"email": { "type": ["string", "null"] }
								}
							},
							"googlePlaceId": {
								"type": ["string", "null"],
								"description": "Google Place ID pointer only — not a dump of Google content."
							}
						}
					}
				]
			},
			"SearchEnvelope": {
				"type": "object",
				"required": ["places", "count", "query", "attribution"],
				"properties": {
					"places": {
						"type": "array",
						"items": { "$ref": "#/components/schemas/PublicPlace" }
					},
					"count": { "type": "integer" },
					"query": { "type": "object" },
					"attribution": { "$ref": "#/components/schemas/Attribution" }
				}
			},
			"DetailEnvelope": {
				"type": "object",
				"required": ["place", "count", "query", "attribution"],
				"properties": {
					"place": { "$ref": "#/components/schemas/PublicPlaceDetail" },
					"count": { "type": "integer", "const": 1 },
					"query": {
						"type": "object",
						"properties": { "slug": { "type": "string" } }
					},
					"attribution": { "$ref": "#/components/schemas/Attribution" }
				}
			}
		}
	}
}
