{
 "openapi": "3.0.3",
 "info": {
  "title": "API de MapasUY",
  "version": "1.0.0",
  "description": "El mapa de Uruguay para tus apps: teselas vectoriales y fotos aéreas, buscador de direcciones, padrones de Catastro, potencial solar y capas en vivo (aviones, barcos, focos de calor y cámaras).\n\n**Clave.** Los endpoints de datos (`/api/buscar`, `/api/padron`, `/api/solar`) piden una clave en el header `X-Clave` (empieza con `muy_`). Pedí una prueba gratis de 1 día en [tu cuenta](https://mapasuy.com/cuenta/) o un plan pago en [Contacto](https://mapasuy.com/#contacto). Cada consulta se cuenta por mes y por clave. Usá la clave desde tu servidor, no la pongas en código que corre en el navegador.\n\n**Gratis y sin clave:** las teselas del mapa, las fotos aéreas y las capas en vivo.\n\n**Créditos obligatorios:** © OpenStreetMap contributors / © OpenMapTiles (mapa), IDE Uruguay (fotos aéreas y padrones), adsb.lol (aviones), NASA FIRMS (focos), AISStream (barcos), PVGIS de la Comisión Europea (sol).",
  "contact": {
   "name": "MapasUY",
   "url": "https://mapasuy.com/#contacto",
   "email": "hola@mapasuy.com"
  }
 },
 "servers": [
  {
   "url": "https://mapasuy.com",
   "description": "Producción"
  }
 ],
 "tags": [
  {
   "name": "Mapas",
   "description": "Teselas vectoriales de Uruguay (esquema OpenMapTiles) y fotos aéreas de la IDE Uruguay. Gratis, sin clave, con CORS abierto."
  },
  {
   "name": "Direcciones",
   "description": "Buscador de direcciones, esquinas, calles, barrios, localidades y lugares conocidos."
  },
  {
   "name": "Padrones",
   "description": "Padrón de Catastro (urbano o rural) en un punto, vía IDE Uruguay."
  },
  {
   "name": "Capas en vivo",
   "description": "Aviones (ADS-B), barcos (AIS), focos de calor (NASA FIRMS) y cámaras públicas. Gratis, sin clave, con CORS abierto."
  },
  {
   "name": "Sol",
   "description": "Potencial de energía solar de un punto y ahorro estimado con la tarifa de UTE."
  }
 ],
 "components": {
  "securitySchemes": {
   "XClave": {
    "type": "apiKey",
    "in": "header",
    "name": "X-Clave",
    "description": "Tu clave de MapasUY (`muy_...`). Prueba gratis de 1 día en https://mapasuy.com/cuenta/ · planes pagos en https://mapasuy.com/#contacto"
   },
   "ClaveQuery": {
    "type": "apiKey",
    "in": "query",
    "name": "clave",
    "description": "Alternativa al header: la clave como parámetro `?clave=muy_...`. Preferí el header: las URLs quedan en logs e historiales."
   }
  },
  "schemas": {
   "Error": {
    "type": "object",
    "required": [
     "ok",
     "error"
    ],
    "properties": {
     "ok": {
      "type": "boolean",
      "example": false
     },
     "error": {
      "type": "string",
      "description": "Mensaje para mostrarle a la persona, en castellano."
     }
    }
   },
   "Resultado": {
    "type": "object",
    "required": [
     "nombre",
     "donde",
     "tipo",
     "lon",
     "lat",
     "zoom"
    ],
    "properties": {
     "nombre": {
      "type": "string",
      "example": "Avenida 18 De Julio 1360"
     },
     "donde": {
      "type": "string",
      "description": "Localidad, barrio y/o departamento.",
      "example": "Montevideo"
     },
     "tipo": {
      "type": "string",
      "enum": [
       "Dirección",
       "Esquina",
       "Calle",
       "Ciudad",
       "Pueblo",
       "Barrio",
       "Caserío",
       "Paraje",
       "Lugar"
      ]
     },
     "lon": {
      "type": "number",
      "format": "double"
     },
     "lat": {
      "type": "number",
      "format": "double"
     },
     "zoom": {
      "type": "integer",
      "description": "Zoom sugerido para mostrar el resultado (13 ciudad, 15 barrio/pueblo, 17 calle o lugar, 18 dirección o esquina)."
     }
    }
   },
   "Busqueda": {
    "type": "object",
    "required": [
     "resultados"
    ],
    "properties": {
     "resultados": {
      "type": "array",
      "maxItems": 8,
      "items": {
       "$ref": "#/components/schemas/Resultado"
      }
     }
    }
   },
   "Padron": {
    "type": "object",
    "required": [
     "ok",
     "encontrado"
    ],
    "properties": {
     "ok": {
      "type": "boolean"
     },
     "encontrado": {
      "type": "boolean",
      "description": "false si no hay padrón en el punto (por ejemplo, en el agua)."
     },
     "mensaje": {
      "type": "string",
      "description": "Solo cuando `encontrado` es false."
     },
     "tipo": {
      "type": "string",
      "enum": [
       "urbano",
       "rural"
      ]
     },
     "padron": {
      "type": "integer",
      "description": "Número de padrón."
     },
     "departamento": {
      "type": "string",
      "nullable": true
     },
     "localidad": {
      "type": "string",
      "nullable": true,
      "description": "Solo urbanos."
     },
     "manzana": {
      "type": "integer",
      "nullable": true,
      "description": "Solo urbanos."
     },
     "area_m2": {
      "type": "number",
      "nullable": true,
      "description": "Solo urbanos: superficie en m²."
     },
     "seccion_catastral": {
      "type": "integer",
      "nullable": true,
      "description": "Solo rurales."
     },
     "area_ha": {
      "type": "number",
      "nullable": true,
      "description": "Solo rurales: hectáreas enteras."
     },
     "area_m2_resto": {
      "type": "number",
      "nullable": true,
      "description": "Solo rurales: m² que se suman a `area_ha`."
     },
     "valor_real": {
      "description": "Solo rurales: valor real de Catastro en pesos (con clave). Sin clave viene el texto \"disponible con clave\".",
      "oneOf": [
       {
        "type": "number"
       },
       {
        "type": "string"
       }
      ],
      "nullable": true
     },
     "nota": {
      "type": "string",
      "description": "Aparece cuando el punto cae en la calle y se devuelve el padrón más cercano (hasta 15 m)."
     },
     "fuente": {
      "type": "string",
      "example": "Dirección Nacional de Catastro, vía IDE Uruguay"
     },
     "modo": {
      "type": "string",
      "enum": [
       "clave",
       "prueba"
      ],
      "description": "`clave` cuando la consulta se hizo con tu clave."
     },
     "consultas_usadas": {
      "type": "integer",
      "description": "Consultas usadas este mes con tu clave (incluida esta)."
     }
    }
   },
   "Solar": {
    "type": "object",
    "required": [
     "ok"
    ],
    "properties": {
     "ok": {
      "type": "boolean"
     },
     "kwp": {
      "type": "number",
      "description": "Potencia instalada del cálculo (siempre 1 kWp: multiplicá por los kWp de tu instalación)."
     },
     "inclinacion": {
      "type": "integer",
      "description": "Inclinación de los paneles en grados."
     },
     "orientacion": {
      "type": "string",
      "example": "norte"
     },
     "kwh_anio": {
      "type": "integer",
      "description": "Energía producida por año (kWh) con 1 kWp."
     },
     "sol_kwh_m2_anio": {
      "type": "integer",
      "description": "Radiación anual sobre el plano de los paneles (kWh/m²)."
     },
     "meses": {
      "type": "array",
      "minItems": 12,
      "maxItems": 12,
      "items": {
       "type": "integer"
      },
      "description": "kWh producidos en cada mes, de enero a diciembre."
     },
     "precio_kwh": {
      "type": "number",
      "description": "Precio del kWh usado para el ahorro (UTE Triple Horario, llano, con IVA), en pesos."
     },
     "ahorro_pesos_anio": {
      "type": "integer",
      "description": "Ahorro aproximado por año en pesos uruguayos."
     },
     "km_auto_electrico": {
      "type": "integer",
      "description": "Kilómetros que haría un auto eléctrico con esa energía (~16 kWh cada 100 km)."
     },
     "pliego": {
      "type": "string",
      "nullable": true,
      "description": "Pliego tarifario de UTE usado."
     },
     "fuente": {
      "type": "string"
     },
     "nota": {
      "type": "string"
     }
    }
   },
   "Avion": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "description": "Código ICAO hexadecimal de 6 caracteres. Usalo en `/api/avion?id=`."
     },
     "vuelo": {
      "type": "string",
      "description": "Indicativo del vuelo (puede venir vacío)."
     },
     "matricula": {
      "type": "string",
      "nullable": true
     },
     "tipo": {
      "type": "string",
      "nullable": true,
      "description": "Código ICAO del modelo (ej. B738, A320)."
     },
     "lat": {
      "type": "number"
     },
     "lon": {
      "type": "number"
     },
     "alt_m": {
      "type": "integer",
      "nullable": true,
      "description": "Altitud barométrica en metros (0 si está en tierra)."
     },
     "kmh": {
      "type": "integer",
      "nullable": true,
      "description": "Velocidad sobre el suelo en km/h."
     },
     "rumbo": {
      "type": "integer",
      "nullable": true,
      "description": "Rumbo en grados (0 = norte)."
     }
    }
   },
   "Aviones": {
    "type": "object",
    "properties": {
     "hora": {
      "type": "string",
      "format": "date-time"
     },
     "fuente": {
      "type": "string"
     },
     "aviones": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Avion"
      }
     }
    }
   },
   "Recorrido": {
    "type": "object",
    "properties": {
     "ok": {
      "type": "boolean"
     },
     "id": {
      "type": "string",
      "nullable": true
     },
     "matricula": {
      "type": "string",
      "nullable": true
     },
     "modelo": {
      "type": "string",
      "nullable": true
     },
     "puntos": {
      "type": "array",
      "maxItems": 601,
      "description": "Puntos del tramo actual, en orden: [lon, lat, altitud en metros]. Hasta ~600 puntos.",
      "items": {
       "type": "array",
       "minItems": 3,
       "maxItems": 3,
       "items": {
        "type": "number"
       }
      }
     }
    }
   },
   "Foco": {
    "type": "object",
    "properties": {
     "lat": {
      "type": "number"
     },
     "lon": {
      "type": "number"
     },
     "fecha": {
      "type": "string",
      "format": "date"
     },
     "hora_utc": {
      "type": "string",
      "example": "17:52"
     },
     "satelite": {
      "type": "string",
      "example": "VIIRS S-NPP"
     },
     "confianza": {
      "type": "string",
      "example": "media"
     },
     "potencia_mw": {
      "type": "number",
      "nullable": true,
      "description": "Potencia radiativa del fuego (MW)."
     },
     "dia": {
      "type": "boolean",
      "description": "true si se detectó de día."
     }
    }
   },
   "Focos": {
    "type": "object",
    "properties": {
     "generado": {
      "type": "string"
     },
     "fuente": {
      "type": "string"
     },
     "nota": {
      "type": "string"
     },
     "focos": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Foco"
      }
     }
    }
   },
   "Camara": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "fuente": {
      "type": "string",
      "description": "Clave dentro de `fuentes`."
     },
     "nombre": {
      "type": "string"
     },
     "detalle": {
      "type": "string"
     },
     "lat": {
      "type": "number"
     },
     "lon": {
      "type": "number"
     },
     "aprox": {
      "type": "boolean",
      "description": "true si la ubicación es aproximada."
     },
     "url": {
      "type": "string",
      "format": "uri",
      "description": "Página oficial de la cámara."
     },
     "stream": {
      "type": "string",
      "format": "uri",
      "description": "Video en vivo HLS (.m3u8)."
     },
     "en_vivo": {
      "type": "boolean",
      "nullable": true,
      "description": "Si la cámara estaba transmitiendo en la última revisión (cada 5 min). null si no se sabe."
     }
    }
   },
   "Camaras": {
    "type": "object",
    "properties": {
     "actualizado": {
      "type": "string"
     },
     "nota": {
      "type": "string"
     },
     "fuentes": {
      "type": "object",
      "additionalProperties": {
       "type": "object",
       "properties": {
        "nombre": {
         "type": "string"
        },
        "web": {
         "type": "string",
         "format": "uri"
        }
       }
      }
     },
     "camaras": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Camara"
      }
     },
     "revisado": {
      "type": "string",
      "format": "date-time"
     }
    }
   },
   "Barco": {
    "type": "object",
    "properties": {
     "id": {
      "type": "integer",
      "description": "MMSI."
     },
     "nombre": {
      "type": "string",
      "nullable": true
     },
     "indicativo": {
      "type": "string",
      "nullable": true
     },
     "tipo": {
      "type": "string",
      "enum": [
       "pesca",
       "remolcador",
       "militar",
       "recreo",
       "pasajeros",
       "carga",
       "tanquero",
       "otro"
      ]
     },
     "lat": {
      "type": "number"
     },
     "lon": {
      "type": "number"
     },
     "kmh": {
      "type": "number",
      "nullable": true
     },
     "rumbo": {
      "type": "number",
      "nullable": true
     },
     "destino": {
      "type": "string",
      "nullable": true
     },
     "eslora": {
      "type": "number",
      "nullable": true,
      "description": "Largo en metros."
     },
     "visto": {
      "type": "string",
      "format": "date-time",
      "description": "Último mensaje AIS recibido."
     }
    }
   },
   "Barcos": {
    "type": "object",
    "properties": {
     "hora": {
      "type": "string",
      "format": "date-time"
     },
     "fuente": {
      "type": "string"
     },
     "viejo": {
      "type": "boolean",
      "description": "Aparece en true si los datos tienen más de 5 minutos."
     },
     "barcos": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Barco"
      }
     }
    }
   }
  }
 },
 "paths": {
  "/uruguay/{z}/{x}/{y}.mvt": {
   "get": {
    "tags": [
     "Mapas"
    ],
    "operationId": "teselaVectorial",
    "summary": "Tesela vectorial del mapa de Uruguay",
    "description": "Teselas vectoriales (Mapbox Vector Tile) con el esquema **OpenMapTiles**, zoom 0 a 14 (a más zoom, MapLibre sobre-amplía la 14). Sirven directo con MapLibre GL, Mapbox GL o cualquier estilo compatible con OpenMapTiles (por ejemplo, OpenFreeMap Liberty: reemplazá la fuente `openmaptiles`). Gratis, sin clave, con CORS abierto y caché de 1 día.\n\n**Crédito obligatorio:** © OpenStreetMap contributors · © OpenMapTiles.",
    "security": [],
    "parameters": [
     {
      "name": "z",
      "in": "path",
      "required": true,
      "description": "Nivel de zoom (0 a 14).",
      "schema": {
       "type": "integer",
       "minimum": 0,
       "maximum": 14
      },
      "example": 14
     },
     {
      "name": "x",
      "in": "path",
      "required": true,
      "description": "Columna de la tesela (esquema XYZ / Web Mercator, igual que Google y OpenStreetMap).",
      "schema": {
       "type": "integer",
       "minimum": 0
      },
      "example": 5687
     },
     {
      "name": "y",
      "in": "path",
      "required": true,
      "description": "Fila de la tesela (XYZ, origen arriba a la izquierda; no TMS).",
      "schema": {
       "type": "integer",
       "minimum": 0
      },
      "example": 9868
     }
    ],
    "responses": {
     "200": {
      "description": "Tesela en formato protobuf.",
      "headers": {
       "Cache-Control": {
        "schema": {
         "type": "string"
        },
        "example": "public, max-age=86400"
       }
      },
      "content": {
       "application/x-protobuf": {
        "schema": {
         "type": "string",
         "format": "binary"
        }
       }
      }
     },
     "404": {
      "description": "La tesela no existe (zoom mayor a 14 o fuera del rango)."
     }
    }
   }
  },
  "/orto/nac/{z}/{x}/{y}.jpg": {
   "get": {
    "tags": [
     "Mapas"
    ],
    "operationId": "ortofotoNacional",
    "summary": "Foto aérea nacional (todo el país)",
    "description": "Fotos aéreas (ortofotos) de la **IDE Uruguay**, vuelo 2017–2018, servidas con caché propia. Teselas raster de 256×256 en el esquema XYZ. Gratis, sin clave y con CORS abierto. **Crédito obligatorio:** «Ortofotos: IDE Uruguay (2017–2018)».\n\nCobertura nacional, zoom 6 a 18. Fuera de Uruguay o de ese rango de zoom la IDE responde error.",
    "security": [],
    "parameters": [
     {
      "name": "z",
      "in": "path",
      "required": true,
      "description": "Nivel de zoom (6 a 18).",
      "schema": {
       "type": "integer",
       "minimum": 6,
       "maximum": 18
      },
      "example": 14
     },
     {
      "name": "x",
      "in": "path",
      "required": true,
      "description": "Columna de la tesela (esquema XYZ / Web Mercator, igual que Google y OpenStreetMap).",
      "schema": {
       "type": "integer",
       "minimum": 0
      },
      "example": 5687
     },
     {
      "name": "y",
      "in": "path",
      "required": true,
      "description": "Fila de la tesela (XYZ, origen arriba a la izquierda; no TMS).",
      "schema": {
       "type": "integer",
       "minimum": 0
      },
      "example": 9868
     }
    ],
    "responses": {
     "200": {
      "description": "Imagen JPEG.",
      "headers": {
       "Cache-Control": {
        "schema": {
         "type": "string"
        },
        "example": "public, max-age=31536000, immutable"
       }
      },
      "content": {
       "image/jpeg": {
        "schema": {
         "type": "string",
         "format": "binary"
        }
       }
      }
     },
     "400": {
      "description": "Tesela fuera de la cobertura o del rango de zoom."
     }
    }
   }
  },
  "/orto/urb/{z}/{x}/{y}.png": {
   "get": {
    "tags": [
     "Mapas"
    ],
    "operationId": "ortofotoUrbana",
    "summary": "Foto aérea urbana (más detalle en ciudades)",
    "description": "Fotos aéreas (ortofotos) de la **IDE Uruguay**, vuelo 2017–2018, servidas con caché propia. Teselas raster de 256×256 en el esquema XYZ. Gratis, sin clave y con CORS abierto. **Crédito obligatorio:** «Ortofotos: IDE Uruguay (2017–2018)».\n\nMás resolución en zonas urbanas, zoom 14 a 19. PNG con transparencia donde no hay foto: ponela arriba de la nacional.",
    "security": [],
    "parameters": [
     {
      "name": "z",
      "in": "path",
      "required": true,
      "description": "Nivel de zoom (14 a 19).",
      "schema": {
       "type": "integer",
       "minimum": 14,
       "maximum": 19
      },
      "example": 14
     },
     {
      "name": "x",
      "in": "path",
      "required": true,
      "description": "Columna de la tesela (esquema XYZ / Web Mercator, igual que Google y OpenStreetMap).",
      "schema": {
       "type": "integer",
       "minimum": 0
      },
      "example": 5687
     },
     {
      "name": "y",
      "in": "path",
      "required": true,
      "description": "Fila de la tesela (XYZ, origen arriba a la izquierda; no TMS).",
      "schema": {
       "type": "integer",
       "minimum": 0
      },
      "example": 9868
     }
    ],
    "responses": {
     "200": {
      "description": "Imagen PNG (transparente donde no hay cobertura urbana).",
      "headers": {
       "Cache-Control": {
        "schema": {
         "type": "string"
        },
        "example": "public, max-age=31536000, immutable"
       }
      },
      "content": {
       "image/png": {
        "schema": {
         "type": "string",
         "format": "binary"
        }
       }
      }
     },
     "400": {
      "description": "Tesela fuera del rango de zoom o de la cobertura."
     }
    }
   }
  },
  "/api/buscar": {
   "get": {
    "tags": [
     "Direcciones"
    ],
    "operationId": "buscar",
    "summary": "Buscar direcciones, esquinas, calles y lugares",
    "description": "Busca en todo Uruguay y devuelve hasta 8 resultados, del más probable al menos probable:\n\n- **Dirección con número**: `tacuarembó 1530` (buscador oficial de direcciones de la IDE Uruguay). El primer resultado trae `entre` (\"Entre Mercedes y Colonia\", \"Esquina X\" o \"Cerca de X\"), que también va adelante en `donde`.\n- **Esquina**: `18 de julio y ejido` (también `esq` o `esquina`). Entiende calles numeradas de la costa (`calle 18 y 38 atlantida`) y \"rambla\" sola (prueba todos los tramos de la rambla).\n- **Calles, barrios y localidades** (índice propio de OpenStreetMap) y **lugares conocidos** (IDE).\n\n**Ciudad**: la escrita después de una coma (`, minas`) o pegada al final (`... las toscas`); si no hay, se prefieren la ciudad del centro del mapa (`lat`/`lon`), las localidades vecinas y Montevideo. Sin ciudad ni centro, Montevideo.\n\n**Resultados sin coordenadas** (`lat` y `lon` en `null`):\n- `tipo: \"Opción\"`: calle parecida (\"¿Quisiste decir?\") cuando un nombre es ambiguo (`rambla y herrera`). Trae `buscar`: volvé a llamar con ese texto.\n- `tipo: \"Sin esquina\"`: las dos calles existen pero no se cruzan, o una calle no está en la ciudad pedida. Solo para mostrar.\n\nSi `q` tiene menos de 2 letras la respuesta es `{\"resultados\":[]}`. Respuestas en caché 1 hora.",
    "security": [
     {
      "XClave": []
     },
     {
      "ClaveQuery": []
     }
    ],
    "parameters": [
     {
      "name": "q",
      "in": "query",
      "required": true,
      "description": "Texto a buscar (se usan los primeros 100 caracteres; mayúsculas y tildes no importan).",
      "schema": {
       "type": "string",
       "maxLength": 100
      },
      "example": "18 de julio 1360, montevideo"
     },
     {
      "name": "lat",
      "in": "query",
      "required": false,
      "description": "Latitud del centro de tu mapa (opcional, para ordenar por cercanía).",
      "schema": {
       "type": "number",
       "format": "double"
      },
      "example": -34.9057
     },
     {
      "name": "lon",
      "in": "query",
      "required": false,
      "description": "Longitud del centro de tu mapa (opcional).",
      "schema": {
       "type": "number",
       "format": "double"
      },
      "example": -56.1862
     }
    ],
    "responses": {
     "200": {
      "description": "Resultados, del más probable al menos probable.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Busqueda"
        },
        "examples": {
         "Dirección": {
          "summary": "18 de julio 1360, montevideo",
          "value": {
           "resultados": [
            {
             "nombre": "Avenida 18 De Julio 1360",
             "donde": "Montevideo",
             "tipo": "Dirección",
             "lon": -56.186173009905936,
             "lat": -34.90569632999515,
             "zoom": 18
            }
           ]
          }
         },
         "Esquina": {
          "summary": "18 de julio y ejido",
          "value": {
           "resultados": [
            {
             "nombre": "18 De Julio Y Ejido",
             "donde": "Melo, Cerro Largo",
             "tipo": "Esquina",
             "lon": -54.16657885699338,
             "lat": -32.363082391602426,
             "zoom": 18
            }
           ]
          }
         },
         "Barrio": {
          "summary": "pocitos (con centro en Montevideo)",
          "value": {
           "resultados": [
            {
             "nombre": "Pocitos",
             "donde": "Montevideo",
             "tipo": "Barrio",
             "lon": -56.15057,
             "lat": -34.91085,
             "zoom": 15
            },
            {
             "nombre": "Pocitos",
             "donde": "San Rafael, Punta del Este, Maldonado",
             "tipo": "Calle",
             "lon": -54.91391,
             "lat": -34.93361,
             "zoom": 17
            },
            {
             "nombre": "Avenida Doctor Francisco Soca",
             "donde": "Pocitos, Montevideo",
             "tipo": "Calle",
             "lon": -56.15596,
             "lat": -34.90337,
             "zoom": 17
            }
           ]
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Falta la clave, es inválida o venció.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "Sin clave": {
          "summary": "Sin clave",
          "value": {
           "ok": false,
           "error": "Necesitás una clave (header X-Clave). Probala gratis un día en https://mapasuy.com/cuenta/ — documentación: https://mapasuy.com/docs/"
          }
         },
         "Clave inválida": {
          "summary": "Clave inválida",
          "value": {
           "ok": false,
           "error": "Clave inválida o desactivada"
          }
         },
         "Clave vencida": {
          "summary": "Clave vencida",
          "value": {
           "ok": false,
           "error": "Tu clave venció. Pedí un plan en https://mapasuy.com/cuenta/"
          }
         }
        }
       }
      }
     },
     "429": {
      "description": "Llegaste al límite de consultas del mes de tu clave.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "Límite del mes": {
          "summary": "Límite del mes",
          "value": {
           "ok": false,
           "error": "Llegaste al límite de consultas del mes. Escribinos a hola@mapasuy.com"
          }
         }
        }
       }
      }
     },
     "503": {
      "description": "Demasiados pedidos seguidos desde tu IP (más de 40 por segundo, con una ráfaga corta permitida) o el servicio de origen no responde. Esperá unos segundos y reintentá."
     }
    }
   }
  },
  "/api/padron": {
   "get": {
    "tags": [
     "Padrones"
    ],
    "operationId": "padron",
    "summary": "Padrón de Catastro en un punto",
    "description": "Devuelve el padrón (urbano o rural) que contiene el punto, consultando en vivo el parcelario de la Dirección Nacional de Catastro vía IDE Uruguay. Si el punto cae en la calle, devuelve el padrón más cercano a menos de 15 m y lo avisa en `nota`.\n\nLos urbanos traen localidad, manzana y área en m². Los rurales traen sección catastral, área (hectáreas + m²) y **valor real**.\n\nRespuestas en caché 30 días por punto (~1 m). **Crédito obligatorio:** Dirección Nacional de Catastro, vía IDE Uruguay.",
    "security": [
     {
      "XClave": []
     },
     {
      "ClaveQuery": []
     }
    ],
    "parameters": [
     {
      "name": "lat",
      "in": "query",
      "required": true,
      "description": "Latitud en grados decimales (WGS84). Tiene que estar dentro de Uruguay.",
      "schema": {
       "type": "number",
       "format": "double",
       "minimum": -35.1,
       "maximum": -30.0
      },
      "example": -34.9057
     },
     {
      "name": "lon",
      "in": "query",
      "required": true,
      "description": "Longitud en grados decimales (WGS84). Tiene que estar dentro de Uruguay.",
      "schema": {
       "type": "number",
       "format": "double",
       "minimum": -58.6,
       "maximum": -53.0
      },
      "example": -56.1862
     }
    ],
    "responses": {
     "200": {
      "description": "Consulta hecha (mirá `encontrado`).",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Padron"
        },
        "examples": {
         "Urbano": {
          "summary": "Padrón urbano (Montevideo)",
          "value": {
           "ok": true,
           "encontrado": true,
           "tipo": "urbano",
           "padron": 9108,
           "departamento": "MONTEVIDEO",
           "localidad": "MONTEVIDEO",
           "manzana": 351,
           "area_m2": 19951,
           "fuente": "Dirección Nacional de Catastro, vía IDE Uruguay",
           "modo": "clave",
           "consultas_usadas": 17
          }
         },
         "Rural": {
          "summary": "Padrón rural (Durazno), con valor real",
          "value": {
           "ok": true,
           "encontrado": true,
           "tipo": "rural",
           "padron": 1200,
           "departamento": "DURAZNO",
           "seccion_catastral": 10,
           "area_ha": 74,
           "area_m2_resto": 8949,
           "valor_real": 966069,
           "fuente": "Dirección Nacional de Catastro, vía IDE Uruguay",
           "modo": "clave",
           "consultas_usadas": 18
          }
         },
         "En la calle": {
          "summary": "El punto cae en la calle",
          "value": {
           "ok": true,
           "encontrado": true,
           "tipo": "urbano",
           "padron": 9108,
           "departamento": "MONTEVIDEO",
           "localidad": "MONTEVIDEO",
           "manzana": 351,
           "area_m2": 19951,
           "nota": "El punto cae en la calle; es el padrón más cercano (6 m)",
           "fuente": "Dirección Nacional de Catastro, vía IDE Uruguay",
           "modo": "clave",
           "consultas_usadas": 19
          }
         },
         "Sin padrón": {
          "summary": "No hay padrón en el punto",
          "value": {
           "ok": true,
           "encontrado": false,
           "mensaje": "No hay un padrón en ese punto"
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "Faltan `lat`/`lon` o el punto no está en Uruguay.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "Faltan datos": {
          "summary": "Faltan datos",
          "value": {
           "ok": false,
           "error": "Faltan lat y lon"
          }
         },
         "Fuera de Uruguay": {
          "summary": "Fuera de Uruguay",
          "value": {
           "ok": false,
           "error": "El punto no está en Uruguay"
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Falta la clave, es inválida o venció.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "Sin clave": {
          "summary": "Sin clave",
          "value": {
           "ok": false,
           "error": "Necesitás una clave. Pedila en https://mapasuy.com/#contacto"
          }
         },
         "Clave inválida": {
          "summary": "Clave inválida",
          "value": {
           "ok": false,
           "error": "Clave inválida"
          }
         },
         "Clave vencida": {
          "summary": "Clave vencida",
          "value": {
           "ok": false,
           "error": "Tu clave venció. Pedí un plan en https://mapasuy.com/cuenta/"
          }
         }
        }
       }
      }
     },
     "429": {
      "description": "Llegaste al límite de consultas del mes de tu clave.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "Límite del mes": {
          "summary": "Límite del mes",
          "value": {
           "ok": false,
           "error": "Llegaste al límite de consultas del mes. Escribinos a hola@mapasuy.com"
          }
         }
        }
       }
      }
     },
     "503": {
      "description": "Catastro (IDE Uruguay) no responde, o demasiados pedidos seguidos desde tu IP.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "Catastro caído": {
          "summary": "Catastro caído",
          "value": {
           "ok": false,
           "error": "Catastro no responde, probá en un rato"
          }
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/solar": {
   "get": {
    "tags": [
     "Sol"
    ],
    "operationId": "solar",
    "summary": "Potencial solar de un punto",
    "description": "Producción estimada de **1 kWp** de paneles (30° de inclinación, mirando al norte, 14 % de pérdidas) según **PVGIS** (Comisión Europea), y el ahorro aproximado con el precio llano de la tarifa Triple Horario de UTE (con IVA). Para una instalación de N kWp, multiplicá `kwh_anio`, `meses` y `ahorro_pesos_anio` por N.\n\nEl cálculo se guarda 90 días por zona (~1 km). **Crédito obligatorio:** PVGIS © Comisión Europea.",
    "security": [
     {
      "XClave": []
     },
     {
      "ClaveQuery": []
     }
    ],
    "parameters": [
     {
      "name": "lat",
      "in": "query",
      "required": true,
      "description": "Latitud (WGS84), dentro de Uruguay.",
      "schema": {
       "type": "number",
       "format": "double",
       "minimum": -35.2,
       "maximum": -30.0
      },
      "example": -34.88
     },
     {
      "name": "lon",
      "in": "query",
      "required": true,
      "description": "Longitud (WGS84), dentro de Uruguay.",
      "schema": {
       "type": "number",
       "format": "double",
       "minimum": -58.7,
       "maximum": -53.0
      },
      "example": -56.17
     }
    ],
    "responses": {
     "200": {
      "description": "Estimación solar.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Solar"
        },
        "example": {
         "ok": true,
         "kwp": 1,
         "inclinacion": 30,
         "orientacion": "norte",
         "kwh_anio": 1456,
         "sol_kwh_m2_anio": 1860,
         "meses": [
          155,
          136,
          140,
          114,
          90,
          80,
          82,
          97,
          115,
          138,
          151,
          159
         ],
         "precio_kwh": 6.31,
         "ahorro_pesos_anio": 9187,
         "km_auto_electrico": 9100,
         "pliego": "Enero 2026",
         "fuente": "PVGIS (Comisión Europea) · tarifa UTE Triple Horario, precio llano con IVA",
         "nota": "Estimación para 1 kWp (unos 2 paneles, ~5 m²). Un techo típico de casa entra 3 a 6 kWp."
        }
       }
      }
     },
     "400": {
      "description": "Faltan `lat`/`lon` o el punto está fuera de Uruguay.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "Fuera de Uruguay": {
          "summary": "Fuera de Uruguay",
          "value": {
           "ok": false,
           "error": "Punto fuera de Uruguay"
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Falta la clave, es inválida o venció.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "Sin clave": {
          "summary": "Sin clave",
          "value": {
           "ok": false,
           "error": "Necesitás una clave (header X-Clave). Probala gratis un día en https://mapasuy.com/cuenta/ — documentación: https://mapasuy.com/docs/"
          }
         },
         "Clave inválida": {
          "summary": "Clave inválida",
          "value": {
           "ok": false,
           "error": "Clave inválida o desactivada"
          }
         },
         "Clave vencida": {
          "summary": "Clave vencida",
          "value": {
           "ok": false,
           "error": "Tu clave venció. Pedí un plan en https://mapasuy.com/cuenta/"
          }
         }
        }
       }
      }
     },
     "429": {
      "description": "Llegaste al límite de consultas del mes de tu clave.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "Límite del mes": {
          "summary": "Límite del mes",
          "value": {
           "ok": false,
           "error": "Llegaste al límite de consultas del mes. Escribinos a hola@mapasuy.com"
          }
         }
        }
       }
      }
     },
     "503": {
      "description": "PVGIS no responde (reintentá en un rato) o demasiados pedidos seguidos desde tu IP.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "examples": {
         "PVGIS caído": {
          "summary": "PVGIS caído",
          "value": {
           "ok": false,
           "error": "El servicio solar no responde, probá en un rato"
          }
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/aviones": {
   "get": {
    "tags": [
     "Capas en vivo"
    ],
    "operationId": "aviones",
    "summary": "Aviones en vivo cerca de un punto",
    "description": "Aviones con transpondedor ADS-B a unos 460 km (250 millas náuticas) del punto, según **adsb.lol** (datos abiertos). El punto se redondea a una grilla de 2°, así que vecinos cercanos comparten respuesta. Se actualiza cada 15 s.\n\nSin `lat`/`lon` usa el centro de Uruguay. Funciona en cualquier lugar del mundo. Gratis, sin clave, con CORS abierto. **Crédito obligatorio:** adsb.lol.",
    "security": [],
    "parameters": [
     {
      "name": "lat",
      "in": "query",
      "required": false,
      "description": "Latitud del centro (por defecto -32.6).",
      "schema": {
       "type": "number",
       "format": "double",
       "default": -32.6
      },
      "example": -34.8
     },
     {
      "name": "lon",
      "in": "query",
      "required": false,
      "description": "Longitud del centro (por defecto -55.8).",
      "schema": {
       "type": "number",
       "format": "double",
       "default": -55.8
      },
      "example": -56.0
     }
    ],
    "responses": {
     "200": {
      "description": "Aviones en la zona.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Aviones"
        },
        "example": {
         "hora": "2026-10-09T19:48:52-03:00",
         "fuente": "adsb.lol (ADS-B abierto)",
         "aviones": [
          {
           "id": "e082d3",
           "vuelo": "ANS401",
           "matricula": "LV-HKS",
           "tipo": "B738",
           "lat": -35.2323,
           "lon": -60.0267,
           "alt_m": 6988,
           "kmh": 619,
           "rumbo": 58
          },
          {
           "id": "e06543",
           "vuelo": "ARG1684",
           "matricula": "LV-FUC",
           "tipo": "B738",
           "lat": -35.5984,
           "lon": -59.8456,
           "alt_m": 7315,
           "kmh": 738,
           "rumbo": 235
          }
         ]
        }
       }
      }
     },
     "400": {
      "description": "`lat` o `lon` no son números.",
      "content": {
       "application/json": {
        "example": {
         "aviones": []
        }
       }
      }
     },
     "503": {
      "description": "adsb.lol no responde o demasiados pedidos seguidos.",
      "content": {
       "application/json": {
        "example": {
         "aviones": []
        }
       }
      }
     }
    }
   }
  },
  "/api/avion": {
   "get": {
    "tags": [
     "Capas en vivo"
    ],
    "operationId": "avion",
    "summary": "Recorrido del vuelo actual de un avión",
    "description": "Recorrido del tramo actual (desde que despegó) de un avión, según **adsb.lol**. Los puntos vienen como `[lon, lat, altitud_m]`, listos para una línea GeoJSON. Se actualiza cada 30 s; hasta ~600 puntos. Gratis, sin clave, con CORS abierto.",
    "security": [],
    "parameters": [
     {
      "name": "id",
      "in": "query",
      "required": true,
      "description": "Código ICAO hexadecimal de 6 caracteres (el `id` de `/api/aviones`).",
      "schema": {
       "type": "string",
       "pattern": "^[0-9a-fA-F]{6}$"
      },
      "example": "e082d3"
     }
    ],
    "responses": {
     "200": {
      "description": "Recorrido.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Recorrido"
        },
        "example": {
         "ok": true,
         "id": "e082d3",
         "matricula": "LV-HKS",
         "modelo": "BOEING 737-800",
         "puntos": [
          [
           -62.8107,
           -36.6106,
           11278
          ],
          [
           -62.796,
           -36.6036,
           11278
          ],
          [
           -62.7217,
           -36.5679,
           11278
          ]
         ]
        }
       }
      }
     },
     "400": {
      "description": "`id` inválido.",
      "content": {
       "application/json": {
        "example": {
         "ok": false
        }
       }
      }
     },
     "404": {
      "description": "No hay recorrido para ese avión.",
      "content": {
       "application/json": {
        "example": {
         "ok": false,
         "puntos": []
        }
       }
      }
     }
    }
   }
  },
  "/api/focos": {
   "get": {
    "tags": [
     "Capas en vivo"
    ],
    "operationId": "focos",
    "summary": "Focos de calor de las últimas 24 horas",
    "description": "Focos de calor detectados por satélite (VIIRS y MODIS) en Uruguay y alrededores en las últimas 24 h, según **NASA FIRMS**. Pueden ser incendios, quemas o industrias. Se actualiza cada hora (caché 10 min). Gratis, sin clave, con CORS abierto. **Crédito obligatorio:** NASA FIRMS.",
    "security": [],
    "responses": {
     "200": {
      "description": "Focos.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Focos"
        },
        "example": {
         "generado": "2026-10-09T19:25:28",
         "fuente": "NASA FIRMS (VIIRS y MODIS), últimas 24 h",
         "nota": "Focos de calor detectados por satélite: pueden ser incendios, quemas o industrias.",
         "focos": [
          {
           "lat": -34.5248,
           "lon": -58.6203,
           "fecha": "2026-10-08",
           "hora_utc": "05:29",
           "satelite": "VIIRS S-NPP",
           "confianza": "media",
           "potencia_mw": 0.58,
           "dia": false
          },
          {
           "lat": -32.1149,
           "lon": -54.4,
           "fecha": "2026-10-08",
           "hora_utc": "17:52",
           "satelite": "VIIRS S-NPP",
           "confianza": "media",
           "potencia_mw": 3.93,
           "dia": true
          }
         ]
        }
       }
      }
     },
     "503": {
      "description": "Todavía no hay datos.",
      "content": {
       "application/json": {
        "example": {
         "focos": []
        }
       }
      }
     }
    }
   }
  },
  "/api/camaras": {
   "get": {
    "tags": [
     "Capas en vivo"
    ],
    "operationId": "camaras",
    "summary": "Cámaras públicas en vivo",
    "description": "Cámaras públicas oficiales (por ahora, Intendencia de Canelones) con ubicación, página oficial, video HLS y si están transmitiendo. El estado se revisa cada 5 min. Mostrá siempre el crédito del organismo dueño de la cámara (`fuentes`). Gratis, sin clave, con CORS abierto.",
    "security": [],
    "responses": {
     "200": {
      "description": "Cámaras.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Camaras"
        },
        "example": {
         "actualizado": "2026-10-09",
         "nota": "Cámaras públicas oficiales en vivo. Se muestra el reproductor de cada organismo con su crédito. aprox=true: ubicación aproximada.",
         "fuentes": {
          "imcanelones": {
           "nombre": "Intendencia de Canelones",
           "web": "https://camaras.imcanelones.gub.uy/"
          }
         },
         "camaras": [
          {
           "id": "can01",
           "fuente": "imcanelones",
           "nombre": "Lagomar",
           "detalle": "Bajada 16 · Rambla y Bullrich",
           "lat": -34.83574,
           "lon": -55.9847,
           "aprox": true,
           "url": "https://camaras.imcanelones.gub.uy/paginas/01.html",
           "stream": "https://camaras.imcanelones.gub.uy/camaras/cam01.m3u8",
           "en_vivo": true
          },
          {
           "id": "can02",
           "fuente": "imcanelones",
           "nombre": "Villa Argentina",
           "detalle": "Playa El Águila",
           "lat": -34.770703,
           "lon": -55.778857,
           "url": "https://camaras.imcanelones.gub.uy/paginas/02.html",
           "stream": "https://camaras.imcanelones.gub.uy/camaras/cam02.m3u8",
           "en_vivo": true
          }
         ],
         "revisado": "2026-10-09T19:49:05-03:00"
        }
       }
      }
     }
    }
   }
  },
  "/api/barcos": {
   "get": {
    "tags": [
     "Capas en vivo"
    ],
    "operationId": "barcos",
    "summary": "Barcos en vivo (AIS)",
    "description": "Barcos con transpondedor AIS en aguas de Uruguay y el Río de la Plata, según **AISStream** (datos abiertos). Se actualiza cada 20 s. Si los datos tienen más de 5 minutos viene `\"viejo\": true`. Gratis, sin clave, con CORS abierto. **Crédito obligatorio:** AISStream.",
    "security": [],
    "responses": {
     "200": {
      "description": "Barcos.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Barcos"
        },
        "example": {
         "hora": "2026-10-09T19:48:59-03:00",
         "fuente": "AISStream.io (AIS abierto)",
         "barcos": [
          {
           "id": 701516000,
           "nombre": "GC-64 MAR DEL PLATA",
           "indicativo": "L2AL",
           "tipo": "otro",
           "lat": -34.41602,
           "lon": -58.57944,
           "kmh": 0,
           "rumbo": 80,
           "destino": "SAN ANTONIO",
           "eslora": 26,
           "visto": "2026-10-09T19:48:46-03:00"
          },
          {
           "id": 701006344,
           "nombre": "VIRAZON",
           "indicativo": "LW3173",
           "tipo": "otro",
           "lat": -34.42666,
           "lon": -58.5554,
           "kmh": 0.4,
           "rumbo": 139,
           "destino": null,
           "eslora": 68,
           "visto": "2026-10-09T19:48:37-03:00"
          }
         ]
        }
       }
      }
     },
     "503": {
      "description": "El servicio de barcos todavía no tiene datos.",
      "content": {
       "application/json": {
        "example": {
         "barcos": []
        }
       }
      }
     }
    }
   }
  }
 }
}