# Справочник Astro API

REST API поверх расчётного ядра астрологии (движок Swiss Ephemeris, полная
точность `.se1`, встроенный — без файлов данных во время выполнения). Все расчёты
геоцентрические; позиции — эклиптические долготы в градусах. Тот же самый
вычислительный код также работает на стороне клиента как модуль WebAssembly (см.
[`../README.md`](../README.md)); этот документ описывает HTTP-поверхность, которую
браузер использует как запасной вариант, пока не загрузится движок WASM, и для
поиска городов.

**Адрес сервера: <https://astro.yuretz.ru>** — рабочее развёртывание; каждый
пример на этой странице обращается непосредственно к нему и может быть запущен как есть.

- **Базовый URL:** `https://astro.yuretz.ru` (продакшн) · `http://127.0.0.1:8431` (локально, см. [`DEPLOY.md`](DEPLOY.md))
- **Тип содержимого:** `application/json` в запросе/ответе, кроме `/v1/geo/pack` (двоичный, gzip)
- **Время:** все временные метки в **UTC**, RFC 3339 (`1990-05-15T09:20:00Z`)
- **Аутентификация:** нет — персональное развёртывание с единственным арендатором
- **Версионирование:** на основе пути, текущий префикс `/v1` (без версии: `/health`)
- **CORS:** открыт — разрешён любой Origin (`GET`/`POST`/`OPTIONS`); браузерные клиенты с других доменов обращаются к API напрямую (API публичный и без аутентификации, кук нет)

### Самостоятельно размещённая документация

Работающий сервер отдаёт собственную документацию (внешние инструменты не нужны):

| URL | Что это |
|---|---|
| [`/docs`](https://astro.yuretz.ru/docs) | Этот справочник, отрендеренный в HTML |
| [`/docs/swagger`](https://astro.yuretz.ru/docs/swagger) | Интерактивный обозреватель API + try-it — полностью самодостаточный (без CDN/внешних запросов) |
| [`/openapi.yaml`](https://astro.yuretz.ru/openapi.yaml) | Спецификация OpenAPI 3.1 (машиночитаемая) |
| [`/openapi.json`](https://astro.yuretz.ru/openapi.json) | Та же спецификация в формате JSON |
| [`/docs/api.md`](https://astro.yuretz.ru/docs/api.md) | Этот файл, необработанный Markdown |

Спецификация OpenAPI ([`../openapi.yaml`](../openapi.yaml)) охватывает каждую
конечную точку ниже и поддерживается в синхронизации с этим документом.

## Соглашения

### Орбы

Орбы аспектов (по каждому аспекту, по каждому телу, по каждому типу карты) и как
их переопределять имеют собственную страницу: [**Орбы**](/docs/orbs). Коротко:
сетка `aspects` по умолчанию использует орбы по каждому аспекту
(☌☍ 8°, △□ 7°, ✶ 5°, ⚻ 3°); отправьте `orbs` для классического метода полусумм
орбов (moiety) по каждому телу, `wide_aspects` для интерпретационных орбов,
`minor_aspects` для гармонических минорных.

### Ошибки

Доменные ошибки (некорректный ввод, сбой эфемерид/движка) возвращают JSON-конверт:

```json
{ "error": "houses undefined at latitude 89.9000: engine reported houses undefined" }
```

| HTTP-статус | Значение |
|---|---|
| `422 Unprocessable Entity` | Некорректный ввод (например, неразрешимый часовой пояс, значение вне диапазона) или дома не определены для заданной широты (полярные регионы с квадрантной системой домов) |
| `500 Internal Server Error` | Движок Swiss Ephemeris сообщил о сбое |

**Некорректно сформированные запросы никогда не достигают доменного слоя** и
используют собственный формат отклонения axum (тело в виде обычного текста, а не
конверт `{"error": …}`):

| HTTP-статус | Причина | Пример тела |
|---|---|---|
| `422 Unprocessable Entity` | Тело JSON не соответствует ожидаемой форме (отсутствующее/неверно типизированное поле) | `Failed to deserialize the JSON body into the target type: missing field `datetime_utc` at line 1 column 2` |
| `400 Bad Request` | Строка запроса не соответствует ожидаемой форме (отсутствующий/неверно типизированный параметр) | `Failed to deserialize query string: missing field `lon`` |

Неизвестные пути **не** дают 404: сервер откатывается к отдаче встроенного
одностраничного приложения (`200 OK`, `text/html`), чтобы работала
клиентская маршрутизация/прямые ссылки. Для API-маршрутов тоже нет
всеохватного 404 — путь с опечаткой под `/v1/...` также получает откат к SPA,
а не JSON-ошибку.

### Общие типы

Используются в запросах и ответах natal/transits/synastry/solar.

**`GeoLocation`**

| Поле | Тип | Обязательный | Примечания |
|---|---|---|---|
| `latitude` | number | да | Градусы, положительные к северу |
| `longitude` | number | да | Градусы, положительные к востоку |
| `altitude` | number | нет (по умолчанию `0`) | Метры над уровнем моря |

**`house_system`** (строковый enum, по умолчанию `"koch"`): `koch` · `placidus` ·
`whole_sign` · `equal` · `regiomontanus` · `campanus`

**`zodiac`** (строковый enum, по умолчанию `"tropical"`): `tropical` · `sidereal`
(айанамша Лахири)

**`objects`** (массив ключей тел, необязательный — по умолчанию стандартные 12):
`sun` `moon` `mercury` `venus` `mars` `jupiter` `saturn` `uranus` `neptune`
`pluto` `mean_node` `true_node` `mean_lilith` `true_lilith` `chiron` `south_node`
`white_moon` `selena` `proserpina`. Набор по умолчанию — всё перечисленное выше,
кроме `true_node`, `true_lilith`, `chiron`, `south_node`, `white_moon`, `selena`,
`proserpina` (для них нужны явные эфемериды астероидов/фиктивных тел, и они
включаются по запросу). `mean_lilith` — средняя Чёрная Луна; `true_lilith` —
истинная (оскулирующая) Чёрная Луна. `south_node` вычисляется (противоположен `true_node`), это
не сырое тело движка. Если запрошенное тело не может быть вычислено (например,
отсутствует файл эфемерид), оно молча **исключается** из ответа, а не приводит к
сбою всей карты.

**`ObjectPosition`** (одна запись на каждое запрошенное тело, в массиве `objects`
каждого ответа с картой):

| Поле | Тип | Примечания |
|---|---|---|
| `body` | string | Ключ тела, как выше |
| `longitude` | number | Эклиптическая долгота, градусы `[0, 360)` |
| `speed_longitude` | number | Градусы/сутки; отрицательное ⇒ ретроградный |
| `sign` | string | `aries` … `pisces` |
| `degrees_in_sign` | number | `[0, 30)` |
| `retrograde` | boolean | Всегда `false` для `sun`/`moon` независимо от знака скорости |
| `dignity` | string \| omitted | Эссенциальное достоинство (западная классика): `domicile` · `exaltation` · `detriment` · `fall` · `peregrine`. **Только десять планет** — исключается для математических точек (узлы/Лилит/жребии) и Хирона |
| `dignity_school` | object \| omitted | Достоинство по школе Ананды: `{ category, score }`, category ∈ `domicile_major·domicile_minor·exaltation·kinship·neutral·enmity·fall·detriment`, score 0–6 (domicile 6 · exaltation 5 · kinship 4 · neutral 3 · enmity 2 · fall 1 · detriment 0). Присутствует для десяти планет, Хирона, Прозерпины и узлов. **⚠️ Одна из нескольких версий школы** — см. примечание ниже |
| `term_ruler` | string | Египетский управитель терма (границы) на этом градусе (позиционный; присутствует для всех объектов) |
| `decan_ruler` | string | Халдейский управитель декана (лица) на этом градусе (позиционный) |
| `speed_class` | string \| omitted | `fast` / `slow` (относительно среднего суточного движения тела). Только физические тела — исключается для математических точек |
| `stationary` | boolean \| omitted | Вот-вот станет станцией (`\|speed\|` менее ~10 % от среднего). Только физические тела — исключается для математических точек |
| `house` | integer \| omitted | `1..12` — присутствует только когда у карты есть дома |
| `dispositor` | string \| omitted | Основной управитель знака объекта по `rulership_scheme` из запроса |
| `solar_status` | string \| omitted | `cazimi` (<0°17′) · `combust` (<8.5°) · `under_beams` (<17°) · `free` — относительно Солнца. **Только десять планет** — исключается для математических точек и Хирона |
| `declination` | number \| omitted | Экваториальное склонение, градусы (для параллели/контрпараллели; исключается для производных точек) |
| `unaspected` | boolean \| omitted | Не образует ни одного птолемеевского аспекта (соединение/секстиль/квадрат/трин/оппозиция — квинконс исключён) к другой из десяти планет; присутствует для десяти планет |

Поля `solar_status` (относительно Солнца) и `dispositor` присутствуют всякий раз,
когда набор объектов включает Солнце (то есть для объектов карты и транзитного
кольца). Значением `body` также может быть `part_fortune` — Жребий Фортуны,
вычисляемая точка, добавляемая в `objects`, когда присутствуют дома (день: Asc +
Луна − Солнце; ночь: Asc + Солнце − Луна).

**Математические точки против физических тел.** Лунные узлы, Лилит, авестийские
точки (Белая Луна / Селена / Прозерпина) и жребии (Жребий Фортуны) не имеют
физического тела, поэтому их **физические атрибуты / атрибуты достоинства
исключаются**: `dignity`, `solar_status`, `speed_class`, `stationary`. Остаются
только позиционные поля — `longitude`, `sign`, `degrees_in_sign`, `house`,
`term_ruler`, `decan_ruler`, `dispositor`, `retrograde`. Хирон — реальное тело: у
него сохраняются `speed_class`/`stationary`; но классическое достоинство/сожжение
для него не определены, поэтому `dignity`/`solar_status` для Хирона тоже
исключаются.

> **`dignity_school` предварительно — одна из нескольких версий школы.** Оно
> закодировано из источника «определений» школы (единичные управления + формула
> тригона: изгнание = напротив управления, kinship = трин к управлению, enmity =
> напротив kinship). Три документа школы Ананды в настоящее время **противоречат**
> друг другу, и это поле следует версии определений в ожидании собственного
> арбитража школы. Известные междокументные конфликты: (1) ось Овен–Скорпион
> (Овен→Марс против Овен→Плутон-основной); (2) экзальтации Нептуна и Плутона
> (Нептун-Водолей/Плутон-Лев против обратного); (3) единичное против двойного
> управления для высших планет/Хирона (двойное даёт обоим знакам score 6); (4)
> вторые изгнания светил (Солнце-Козерог и Луна-Водолей с оценкой 1, а не neutral).
> Не считайте эти ячейки окончательными.

**`Houses`** (присутствует в поле `houses` карты только когда была задана
`location`):

| Поле | Тип | Примечания |
|---|---|---|
| `system` | string | Повторяет запрошенную `house_system` |
| `cusps` | number[12] | Долготы куспидов домов I … XII, градусы |
| `ascendant` | number | Градусы |
| `midheaven` | number | Градусы |
| `descendant` | number | Градусы; = `ascendant` + 180° |
| `ic` | number | Градусы; Imum Coeli = `midheaven` + 180° |
| `vertex` | number \| null | Градусы, когда доступно |
| `intercepted_signs` | string[] | Знаки, полностью заключённые внутри дома (без куспида внутри) |
| `house_rulers` | array | По каждому дому: `{ house, ruler[], co_rulers[], almuten }` — управитель(и) знака куспида (массив; 1–2 — двойные обители `modern` по секте или оси `spbaa`), управители включённых/занимающих знаков и альмутен градуса куспида |

`NatalChart` также несёт анализ на уровне карты: `sign_rulers` (управитель(и)
каждого из 12 знаков по `rulership_scheme` из запроса; **каждая запись — массив**
из 1–2 тел — два на двойных обителях `modern` (по секте) и осях `spbaa`),
`sect` (`diurnal`/`nocturnal`),
`final_dispositor` (уникальная самодиспонирующая планета в современной схеме, иначе
`null`), `moon_phase` (`waxing`/`waning`), `moon_void` (`{ void, start?, until,
next_sign_at, via_combusta?, spica_exception? }` — Луна не образует больше ни одного
мажорного аспекта до выхода из своего знака; `start` — начало холостого окна
(последний точный аспект Луны в знаке, либо вход в знак, если аспектов не было),
присутствует только при `void: true`; `via_combusta` отмечает Луну в
15° Весов – 15° Скорпиона, а `spica_exception` — средневековую защиту Луны без
курса/поражённой Луны в соединении со Спикой ±1°) и `declination_aspects`
(`[{ a, b, kind: parallel|contraparallel, orb }]`, орб 1°).

**Схемы управления** (`rulership_scheme`, по умолчанию `modern`):
- `traditional` — классические семь; каждый знак имеет одного управителя.
- `modern` (по умолчанию) — три высшие планеты заменяют классического управителя в
  своих знаках, причём три знака имеют **секта-зависимые двойные обители**: Скорпион
  (день Плутон / ночь Марс), Козерог (день Сатурн / ночь Уран), Рыбы (день Нептун /
  ночь Юпитер). Секта карты выбирает активного/главного владыку (первым в массиве),
  но обе планеты держат обитель. **Водолей — фиксированная пара `[uranus, saturn]`**:
  Уран всегда старший управитель, Сатурн всегда со-управитель, секта порядок не
  меняет (решение школы, 2026-07-11). Без домов (нет секты)
  берётся дневной порядок, поэтому дневные карты совпадают с классической
  модернизацией. Двойные обители влияют на `sign_rulers`/`dispositor`/`house_rulers`
  и `disposition`/`receptions`, но **не** на поле `dignity` объекта — оно остаётся
  одиночным классическим (Уран в Козероге — перегринный, а не в обители).
- `spbaa` — санкт-петербургская школа (Сенина). Светила, Меркурий и Венера
  управляют безусловно. **Ось Овен–Скорпион** следует матрице Сениной (единичный
  управитель): и Марс, и Плутон директны → Овен = Плутон, Скорпион = Марс; иначе
  Овен = Марс, Скорпион = Плутон. На **социальных/высших осях** (Юпитер/Нептун,
  Сатурн/Уран) ретроградная планета *приобретает* парный знак, не теряя своего,
  поэтому знак может иметь **двух управителей** (например, ретроградный Юпитер →
  Рыбы = [Нептун, Юпитер]). Именно поэтому `sign_rulers`/`house_rulers.ruler` —
  массивы.

**`domicile_rulers`** (необязательно) — своя день/ночь-таблица обителей. Когда
передана, **переопределяет `rulership_scheme`** для всего низового расчёта: поля
`dignity`, `dispositor`, `sign_rulers`/`house_rulers`, `receptions` и альмутены
считаются по ней. Секта карты выбирает активного управителя (день/ночь-столбец);
**оба владыки знака считаются обителью** (в `dignity` и в баллах альмутена). Схемы
`traditional`/`spbaa` и дефолт движка (без таблицы) остаются классическими.
Два формата:
- **разреженный** — только переопределяемые знаки, остальные = современный одиночный
  управитель: `{"scorpio":{"day":"pluto","night":"mars"}, "capricorn":{"day":"saturn","night":"uranus"}}`;
- **полный** — два массива по 12 (индекс Овен…Рыбы): `{"day":[…12 тел…],"night":[…12 тел…]}`.

Далее она несёт анализ высшего порядка, весь вычисляемый один раз в `astro-core` и
разделяемый API и веб-клиентом:

- **`almuten`** — альмутен карты по методу Ибн Эзры, с воспроизводимой разбивкой:
  `{ winner, method: "ibn_ezra", points_used: [sun, moon, ascendant,
  part_fortune], scores: [{ body, score }], tie }`. В каждой точке каждое
  достоинство начисляет своему управителю очки (domicile 5, exaltation 4,
  triplicity 3, term 2, face 1); побеждает наибольшая сумма. Присутствует с домами.
- **`angle_aspects`** (с домами) — аспекты от объектов ко **всем четырём углам
  карты**: `[{ body, angle: ascendant|midheaven|descendant|imum_coeli, kind,
  exact_angle, orb }]`. Dsc/IC считаются прямо (не зеркалом): каждый хит держит
  орб по умолчанию своего вида аспекта, поэтому 6°-трин к Asc не порождает
  6°-секстиль к Dsc, если секстильный орб уже.
- **`chart_shape`** — планетарный паттерн Марка Эдмунда Джонса (общая форма карты):
  `{ pattern, occupied_arc, largest_gap, leading?, handle? }`, `pattern` ∈
  `splash · bundle · bowl · bucket · locomotive · seesaw · splay`. `leading` —
  планета после наибольшего разрыва (bowl/locomotive/bucket); `handle` —
  изолированная планета(ы) корзины. Вычисляется по десяти планетам.
- **`planetary_hour`** (с местоположением) — классический планетарный час:
  `{ day_ruler, hour_ruler, hour_index (1-12 день / 13-24 ночь), is_day, sunrise,
  sunset, next_sunrise }`. Астрономический день идёт от восхода до заката (12
  неравных дневных часов) и от заката до следующего восхода (12 ночных часов);
  управитель 1-го часа = управитель дня; часы идут по халдейскому порядку.
  Управитель дня — день недели местного дня, на который приходится восход.
- **`lunar_day`** (с местоположением) — лунный день 1–30 **по восходу Луны**
  (школа Вронского/Величко): `{ number, day_start, day_end, new_moon, moonset? }`.
  День 1 идёт от новолуния до первого восхода Луны после него; каждый следующий
  день начинается с восхода Луны; последний день цикла (29/30) кончается
  следующим новолунием. Восход — видимый (та же конвенция, что `sunrise` в
  `planetary_hour`), поэтому номер зависит от места. `moonset` — ближайший заход
  Луны после момента карты (справочно). Отсутствует на полярных широтах без
  восхода Луны. Блок `fields`: `lunar_day` (в профиле `horary` выключен).
- **`moon_sequence`** — последовательность аспектов Луны в пределах её текущего
  знака (хорар): `{ last?, upcoming[], ingress_at }`, каждый аспект
  `{ body, kind, exact }`. `last` — последний точный мажорный аспект перед моментом
  карты (null, если такого нет в этом знаке); `upcoming` — будущие точные аспекты
  вплоть до ингрессии (по возрастанию). Аспектирующие тела — десять планет (кроме
  Луны). **`upcoming == []` тогда и только тогда, когда `moon_void.void`**, а
  `ingress_at == moon_void.next_sign_at`; каждая запись `upcoming` воспроизводится
  через `/v1/aspect-hits` (transit_body=moon).
- **`star_conjunctions`** — соединения неподвижных звёзд с десятью планетами и
  углами карты (Asc/MC/Dsc/IC), по эклиптической долготе:
  `[{ body?|angle?, star, star_longitude, orb, magnitude }]`. ~30 значимых звёзд
  (четыре Королевские звезды + бегенианские звёзды + ярчайшие); орб масштабируется с
  величиной (≤1 → 2°, ≤2 → 1.5°, иначе 1°); долготы звёзд прецессированы на дату
  карты. Отсортировано по орбу.
- **`star_positions`** — полный каталог неподвижных звёзд с долготами, для
  отрисовки звёзд вокруг круга (в стиле ZET):
  `[{ star, longitude, magnitude, anchor }]`, прецессировано на дату карты,
  отсортировано по долготе. Тот же каталог и условие, что у `star_conjunctions`.
  `anchor` отмечает постоянную ориентировочную звезду (четыре Королевские звезды +
  Алголь + Спика); чтобы разгрузить круг, рендереры отрисовывают звезду только когда
  она anchor **или** появляется в `star_conjunctions` (звезда в пустом градусе не
  интерпретируется).
- **`configurations`** — аспектные паттерны: `[{ type, bodies[], apex?, axis?,
  edges: [{a,b}], includes_points?, … }]`, `type` ∈ `grandCross · trapezoid ·
  cart · sail · grandTrine · stellium · yod · tSquare · bisextile`, упорядочено по
  значимости. Орбы рёбер получают **бонус +1°**, когда участвует светило.
  T-square/yod/bisextile несут `apex` (тело, получающее оба одинаковых аспекта), а
  T-square — `axis` (два противоположных полюса). Участвуют только десять планет.
  **Стеллиумы** — 3+ планеты в одном `sign` или `house` (`scope`), с любыми точками
  в этом знаке/доме, перечисленными как `attendants` (не учитываются в кворуме).
- **`receptions`** — взаимные рецепции среди десяти планет:
  `{ by_rulership: [{a,b,scheme,aspect?}], by_exaltation: […], by_face: […] }`.
  `by_rulership` сообщает о схемах `traditional` и `modern`, каждая пара помечена
  своей `scheme`; рецепция по управлению в схеме *S* — это в точности цикл длины 2 в
  диспозиции схемы *S*. Каждая пара несёт `aspect` между двумя телами, когда таковой
  существует.
- **`disposition`** — дерево диспозиции как функциональный граф (каждая планета
  имеет ровно одного диспозитора, поэтому каждая компонента содержит ровно один
  цикл). Возвращает `{ schemes: { modern, spbaa }, by_detriment }`. Каждая схема с
  единственным управителем (`modern`/`by_detriment`) — это
  `{ nodes: [{ body, disposes_to }], terminals: [body,…] (самодиспонирующие,
  циклы длины 1), loops: [[body,…]] (длина ≥ 2; длина 2 = взаимная рецепция),
  chains: [{ body, path, sink: { type: terminal|loop, id } }], final_dispositor }`.
  `final_dispositor` установлен тогда и только тогда, когда есть ровно один терминал
  и нет петель. `modern` — эффективная схема (учитывает `domicile_rulers`, когда
  таблица передана). `by_detriment` диспонирует каждую планету управителем знака,
  **противоположного** её положению. Схема **`spbaa`** — граф со *многими родителями* (двойное
  управление → до двух диспозиторов): `{ nodes: [{ body, disposes_to: [body,…] }],
  terminals, experimental: true }`; семантика петель/цепочек при множественных
  рёбрах отложена.

`POST /v1/charts/transits` и `/v1/charts/synastry` принимают необязательный `orb`
(number): единый максимальный орб для каждого вида аспекта вместо значений по
умолчанию для каждого вида (например, `6` для индекса конфликтности Шестопалова).
Ответ синастрии также включает `a_in_b_houses` и `b_in_a_houses`
(`[{ body, house }]` — планеты каждой карты, помещённые в дома другой) и
кросс-контакты планета↔угол `a_to_b_angles` / `b_to_a_angles` (`AngleAspect[]` —
тела одной карты к четырём углам другой; пусто без домов соответствующей карты).

Составные эндпоинты (`/v1/charts/transits`, `/synastry`, `/solar`, `/return`,
`/progressed`) принимают те же параметры проекции `?profile=&fields=&precision=`,
что и `/v1/charts/natal` (**только в строке запроса**, не в теле) — пресет шейпит
каждую вложенную карту (`natal`/`transit`/`a`/`b`/`solar`/`returned`/`progressed`),
пропуская и вычисление, и сериализацию отброшенных блоков.

**`AspectHit`** (записи в массиве `aspects` любой карты — natal (внутри карты),
transits, synastry, solar):

| Поле | Тип | Примечания |
|---|---|---|
| `a`, `b` | string | Ключи тел двух концов |
| `kind` | string | `conjunction` `sextile` `square` `trine` `opposition` `quincunx` |
| `angle` | number | Точный угол аспекта: 0/60/90/120/150/180 |
| `orb` | number | Отклонение от точного, градусы (`0` = партиль), 2 знака после запятой |
| `applying` | boolean | `true` = сходящийся (орб сокращается), `false` = расходящийся (орб растёт), по долготным скоростям обоих тел |

Орбы по умолчанию: соединение/оппозиция 8°, трин/квадрат 7°, секстиль 5°,
квинконс 3°. Не настраивается через API. Тот же матчер (орбы + правило
сходящийся/расходящийся) используется для каждого списка аспектов здесь **и**
кругом веб-клиента (через движок WASM), поэтому они никогда не расходятся.

---

## `GET /health`

Проба живучести и информация о движке. Без аутентификации, дёшево, без удержания
блокировки движка сверх вызова.

**200 OK**

```json
{
  "status": "ok",
  "service": "astro",
  "engine": "swiss-ephemeris (embedded, in-memory)",
  "precision": "~0.001 arcsec"
}
```

`engine`/`precision` фиксированы для этого развёртывания (данные `.se1` и
хук `swi_fopen` в памяти всегда присутствуют — см.
[`swisseph-embedded.md`](swisseph-embedded.md)); нет деградированного/запасного
режима для сообщения.

```bash
curl -s https://astro.yuretz.ru/health
```

---

## `POST /v1/charts/natal`

Вычислить натальную карту (радикс): позиции тел плюс, когда задано местоположение,
куспиды домов и углы.

### Тело запроса (`NatalRequest`)

| Поле | Тип | Обязательный | По умолчанию | Примечания |
|---|---|---|---|---|
| `datetime_utc` | string (RFC 3339) | да | — | Момент рождения в UTC |
| `location` | `GeoLocation` \| null | нет | `null` | Опустить → `houses` исключается из ответа |
| `house_system` | string | нет | `koch` | См. enum системы домов выше |
| `zodiac` | string | нет | `tropical` | См. enum зодиака выше |
| `objects` | string[] | нет | стандартные 12 | См. список ключей тел выше |
| `wide_aspects` | boolean | нет | `false` | Также выдавать `wide_aspects`/`wide_angle_aspects` с конвенциональными орбами (мажоры 6°, светила 8°, квинконс 3°) с зеркалами Dsc/IC; `derived` отмечает попадание только в расширенной полосе. `aspects`/`angle_aspects` по умолчанию не меняются |
| `orbs` | object | нет | — | Допуски орбов по каждому телу (классический метод полусумм орбов) для **пяти птолемеевых аспектов**; **квинконс** сохраняется со своим фиксированным орбом 3° (не отбрасывается). Присутствует (даже `{}`) → `aspects` используют их вместо значений по умолчанию для каждого аспекта; частичное отображение, опущенные тела сохраняют классические значения по умолчанию. Возвращается обратно в поле `orbs` ответа. См. страницу [**Орбы**](/docs/orbs) |
| `minor_aspects` | boolean | нет | `false` | Также выдавать `minor_aspects` — гармонические миноры (полусекстиль, полуквадрат, квинтиль, семейства септиль/новиль, …) на узких орбах |
| `asc_longitude` | number | нет | — | **Кармическая карта** (метод Шестопалова): строить дома от этой долготы Асцендента (например, ASC = Восходящий узел) вместо горизонта. Система домов сохраняется — для широты места решается соответствующий ARMC, куспиды Коха/Плацидуса осмысленны. Требует `location`; только тропический зодиак |
| `profile` | string | нет | `raw` | Пресет проекции ответа (см. ниже); `?profile=` в запросе имеет приоритет |
| `precision` | integer | нет | по умолчанию профиля | Десятичное округление для числовых полей |
| `fields` | string | нет | — | `+block`/`-block` поверх профиля или голый белый список `a,b` |

### Проекция ответа (необязательно)

`profile` выбирает пресет, который **пропускает и вычисление, и сериализацию**
блоков, которые он отбрасывает — более дешёвый ответ, а не просто отфильтрованный.
Пресеты:

| Профиль | Что он возвращает |
|---|---|
| `raw` (по умолчанию) | Всё; побайтово идентично непроецированному ответу |
| `natal_interp` | Набор для интерпретации: отбрасывает у каждого объекта `speed_longitude`/`declination`, `disposition.by_detriment` и `star_conjunctions`/`star_positions`; округляет до 2 знаков |
| `horary` | Только хорарные основы (дома, объекты, `moon_void`, `planetary_hour`, `moon_sequence`, аспекты, receptions, sign_rulers); 2 знака |
| `calendar` | Только позиции + дома; 2 знака |

`precision` переопределяет округление; `fields=+a,-b` переключает отдельные блоки
поверх профиля (голый `fields=a,b` — явный белый список). Параметры запроса имеют
приоритет над теми же ключами в теле. Неизвестный `profile` даёт `422`.

### Ответ (`NatalChart`) — 200 OK

| Поле | Тип | Примечания |
|---|---|---|
| `datetime_utc` | string | Повторяет запрос |
| `julian_day_ut` | number | Юлианский день, всемирное время |
| `zodiac` | string | Повторяет запрос |
| `objects` | `ObjectPosition[]` | |
| `aspects` | `AspectHit[]` | Аспекты между собственными объектами этой карты (внутри карты) |
| `houses` | `Houses` \| omitted | Присутствует только если была задана `location` |

### Ошибки

- `422` — `houses undefined at latitude …`, когда `location` задана на широте, где
  выбранная квадрантная система домов (Koch/Placidus/…) математически не определена
  (вблизи полюсов). Whole-sign и Equal не затронуты.
- `422` — некорректно сформированное тело запроса (см. [Ошибки](#errors) выше).

### Пример

```bash
curl -s -X POST https://astro.yuretz.ru/v1/charts/natal \
  -H 'content-type: application/json' \
  -d '{
    "datetime_utc": "1990-05-15T09:20:00Z",
    "location": { "latitude": 55.7558, "longitude": 37.6178 },
    "house_system": "placidus",
    "zodiac": "tropical"
  }'
```

```json
{
  "datetime_utc": "1990-05-15T09:20:00Z",
  "julian_day_ut": 2448026.888888889,
  "zodiac": "tropical",
  "objects": [
    { "body": "sun", "longitude": 54.288970350752, "speed_longitude": 0.9642565409, "sign": "taurus", "degrees_in_sign": 24.288970350, "retrograde": false },
    { "body": "moon", "longitude": 295.79073022998, "speed_longitude": 12.329011637, "sign": "capricorn", "degrees_in_sign": 25.790730229, "retrograde": false }
  ],
  "houses": {
    "system": "placidus",
    "cusps": [140.5, 168.2, "…"],
    "ascendant": 140.5,
    "midheaven": 45.1,
    "descendant": 320.5,
    "ic": 225.1,
    "vertex": 210.3
  }
}
```

---

## `GET /v1/charts/natal.svg`

Натальный круг как отдельное SVG-изображение — та же геометрия, шрифты и глифы, что
у веб-клиента — пригодно для непосредственного использования как `<img src>`.
Неподвижные звёзды отрисовываются сразу за зодиакальным кольцом, каждая несёт
нативную всплывающую подсказку `<title>` при наведении (имя + величина); передайте
`stars=false`, чтобы их скрыть. Чтобы избежать загромождения, отрисовываются только
астрологически значимые звёзды: постоянные якоря (четыре Королевские звезды +
Алголь + Спика) плюс любая звезда в соединении с планетой или углом в этой карте.

### Параметры запроса

| Параметр | Тип | Обязательный | По умолчанию | Примечания |
|---|---|---|---|---|
| `datetime_utc` | string (RFC 3339) | да | — | Момент рождения в UTC |
| `lat`, `lon` | number | нет | — | Оба вместе добавляют дома/углы |
| `house_system` | string | нет | `koch` | См. enum системы домов |
| `zodiac` | string | нет | `tropical` | |
| `title` | string | нет | — | Подпись, отрисованная под кругом |
| `stars` | boolean | нет | `true` | Отрисовать неподвижные звёзды за кольцом |
| `theme` | string | нет | `light` | Цветовая схема: `light` или `dark` |

Возвращает `image/svg+xml` (кэшируемо, `max-age=3600`).

### Пример

```
https://astro.yuretz.ru/v1/charts/natal.svg?datetime_utc=1990-05-15T09:20:00Z&lat=55.7558&lon=37.6178&house_system=placidus
https://astro.yuretz.ru/v1/charts/natal.svg?datetime_utc=1990-05-15T09:20:00Z&lat=55.7558&lon=37.6178&theme=dark
```

---

## `POST /v1/charts/transits`

Натальная карта плюс позиции тел на второй момент («транзит»), с меж-аспектами
между двумя наборами.

### Тело запроса (`TransitsRequest`)

| Поле | Тип | Обязательный | Примечания |
|---|---|---|---|
| `natal` | `NatalRequest` | да | Та же форма, что тело `/v1/charts/natal` |
| `transit` | `MomentRequest` | да | `{ "datetime_utc": "…", "location": null }` — `location` принимается, но не используется для вычисления транзитных домов |

### Ответ (`TransitsChart`) — 200 OK

| Поле | Тип | Примечания |
|---|---|---|
| `natal` | `NatalChart` | Как возвращается `/v1/charts/natal` |
| `transit` | object | `{ datetime_utc, julian_day_ut, objects }` — та же форма `objects`, без `houses` |
| `aspects` | `AspectHit[]` | Натал × транзит, все пары, включая совпадение тел (например, транзитное Солнце в соединении с натальным Солнцем) |

### Ошибки

Те же, что у `/v1/charts/natal` (ошибка может исходить из любого момента).

### Пример

```bash
curl -s -X POST https://astro.yuretz.ru/v1/charts/transits \
  -H 'content-type: application/json' \
  -d '{
    "natal": { "datetime_utc": "1990-05-15T09:20:00Z", "location": { "latitude": 55.7558, "longitude": 37.6178 } },
    "transit": { "datetime_utc": "2026-07-02T10:00:00Z" }
  }'
```

---

## `POST /v1/charts/synastry`

Две натальные карты (`a`, `b`) с меж-аспектами между ними.

### Тело запроса (`SynastryRequest`)

| Поле | Тип | Обязательный | Примечания |
|---|---|---|---|
| `a` | `NatalRequest` | да | |
| `b` | `NatalRequest` | да | |

### Ответ (`SynastryChart`) — 200 OK

| Поле | Тип | Примечания |
|---|---|---|
| `a` | `NatalChart` | |
| `b` | `NatalChart` | |
| `aspects` | `AspectHit[]` | `a` × `b`, все пары |

### Ошибки

Те же, что у `/v1/charts/natal` (может исходить из любой карты).

---

## `POST /v1/charts/solar`

Натальная карта плюс соляр на заданный год (момент, когда транзитное Солнце
возвращается к своей натальной эклиптической долготе), с меж-аспектами.

### Тело запроса (`SolarRequest`)

| Поле | Тип | Обязательный | По умолчанию | Примечания |
|---|---|---|---|---|
| `natal` | `NatalRequest` | да | — | |
| `year` | integer | да | — | Календарный год возврата, поиск с ~2 дней до 1 января |
| `location` | `GeoLocation` \| null | нет | `location` натала | Место, используемое для домов карты возврата |

### Ответ (`SolarChart`) — 200 OK

| Поле | Тип | Примечания |
|---|---|---|
| `natal` | `NatalChart` | |
| `solar` | `NatalChart` | Карта возврата — `datetime_utc` это точный момент возврата |
| `aspects` | `AspectHit[]` | Натал × соляр |

### Ошибки

- `422` — `invalid input: natal chart has no Sun to compute a solar return`, если
  `natal.objects` был задан явно и исключает `sun`.
- `422` — `invalid input: invalid year` для `year` вне представимого диапазона.
- Те же ошибки домов/движка, что у `/v1/charts/natal`.

---

## `POST /v1/charts/return`

Карта возврата для любого тела: момент, когда оно в следующий раз достигает своей
натальной долготы ближайший к `around` (лунный возврат, возврат Сатурна, возврат
узлов — то же нахождение корней, что использует соляр, обобщённое).

### Тело запроса (`ReturnRequest`)

| Поле | Тип | Обязательный | Примечания |
|---|---|---|---|
| `natal` | `NatalRequest` | да | |
| `body` | string | да | Возвращающееся тело (`moon`, `saturn`, `mean_node`, …) |
| `around` | string (RFC 3339) | да | Приблизительная дата; ближайший точный возврат ищется в пределах ±15 % периода тела |
| `location` | `GeoLocation` \| null | нет | Дома карты возврата; по умолчанию как у натала |

### Ответ — 200 OK

`{ natal: NatalChart, returned: NatalChart, aspects: AspectHit[] }` — `returned`
это полная карта в момент возврата; `aspects` это натал × returned.

### Ошибки

- `422` — `no return found near the given date` (расширьте `around`), либо в
  натальной карте отсутствует запрошенное тело.

---

## `POST /v1/charts/progressed`

Вторичные прогрессии (1 день эфемерид = 1 год жизни) или символические дирекции
(каждая точка продвигается на фиксированную дугу за год).

### Тело запроса (`ProgressedRequest`)

| Поле | Тип | Обязательный | По умолчанию | Примечания |
|---|---|---|---|---|
| `natal` | `NatalRequest` | да | — | |
| `target` | string (RFC 3339) | да | — | Дата, до которой прогрессировать |
| `method` | string | нет | `secondary` | `secondary` или `symbolic` |
| `rate` | number | нет | `1` | Градусов в год для `symbolic` (1° = 1 год) |

### Ответ — 200 OK

`{ natal, progressed: NatalChart, aspects: AspectHit[], age_years }`.
Secondary использует дробные дни напрямую (без округления дней); symbolic
продвигает каждую долготу и поворачивает дома на дугу.

---

## `POST /v1/charts/directions/primary`

Первичные дирекции — метод Плацидуса (полудуги, зодиакальные). Дуга прямого
восхождения, подводящая промиссор к сигнификатору суточным вращением, переводится в
годы жизни ключом. **Сигнификаторы:** Asc, MC, Солнце, Луна. **Промиссоры:** десять
планет и их птолемеевы аспекты (☌ ✶ □ △ ☍). Считается тропически; требует места
рождения (нужны дома/широта/ARMC). Подробности формул — `/docs/calculations` §16.4а.

### Тело запроса (`PrimaryDirectionsRequest`)

| Поле | Тип | Обязательный | По умолчанию | Примечания |
|---|---|---|---|---|
| `natal` | `NatalRequest` | да | — | Должен содержать `location` |
| `key` | string | нет | `naibod` | Ключ дуга→время: `naibod` (0°59′08″/год) или `ptolemy` (1°/год) |
| `max_years` | number | нет | `90` | Верхняя граница возраста для событий |
| `converse` | boolean | нет | `true` | Также считать конверсные дирекции |

### Ответ — 200 OK

`{ birth, latitude, armc, obliquity, key, events[] }`, где `events` отсортированы по
возрасту, а каждое событие:

| Поле | Тип | Примечания |
|---|---|---|
| `significator` | string | `ascendant` \| `midheaven` \| `sun` \| `moon` |
| `promissor` | string | Тело-промиссор |
| `aspect` | string | `conjunction` `sextile` `square` `trine` `opposition` |
| `motion` | string | `direct` (промиссор ведётся к сигнификатору) \| `converse` |
| `arc` | number | Дуга дирекции в градусах прямого восхождения |
| `years` | number | Возраст перфекции (`arc` / ключ) |
| `date` | string | Календарная дата перфекции (`birth` + `years`) |

---

## `POST /v1/aspect-hits`

Точные даты, когда транзитное тело образует каждый мажорный аспект (соединение,
секстиль, квадрат, трин, оппозиция) к натальным точкам, за диапазон дат, с окнами
входа/выхода из орба. Один вызов заменяет двоичный поиск по `/transits` для «когда
именно?».

### Тело запроса (`AspectHitsRequest`)

| Поле | Тип | Обязательный | По умолчанию | Примечания |
|---|---|---|---|---|
| `natal` | `NatalRequest` | да | — | |
| `transit_body` | string | одно из двух | — | Одно транзитное тело |
| `transit_bodies` | string[] | одно из двух | — | **Батч**: несколько транзитных тел за один вызов — натальная карта считается один раз, каждое событие в `hits`/`angle_hits` несёт своё `transit_body`; дубликаты схлопываются. Имеет приоритет над `transit_body`. Один батч-вызов заменяет N последовательных |
| `from`, `to` | string (RFC 3339) | да | — | Диапазон дат (`to` должен быть после `from`) |
| `orb` | number | нет | `1` | Орб (°) для окна входа/выхода |
| `include_angles` | boolean | нет | `false` | Также находить корни точных аспектов к углам карты (Asc/MC + зеркала Dsc/IC) → `angle_hits` |

### Ответ (`AspectHits`) — 200 OK

`{ orb, hits: [...], angle_hits?: [...] }`, каждый список отсортирован по `exact`:

| Поле | Тип | Примечания |
|---|---|---|
| `transit_body`, `natal_body` | string | Два конца |
| `kind` | string | Вид аспекта |
| `angle` | number | 0/60/90/120/180 |
| `exact` | string | Момент точного попадания (UTC) |
| `enters`, `leaves` | string | Когда тело входит в / выходит из окна `orb` (линейная оценка по скорости в точный момент) |

При `include_angles` `angle_hits[]` добавляет `{ transit_body, angle (ascendant/
midheaven/descendant/imum_coeli), kind, exact_angle, exact, enters, leaves,
derived? }` — записи Dsc/IC это зеркала (`derived: true`) пересечения Asc/MC.

### Пример

```bash
curl -s -X POST https://astro.yuretz.ru/v1/aspect-hits \
  -H 'content-type: application/json' \
  -d '{"natal":{"datetime_utc":"1990-05-15T09:20:00Z","location":{"latitude":55.7558,"longitude":37.6178}},
       "transit_body":"saturn","from":"2026-01-01T00:00:00Z","to":"2027-01-01T00:00:00Z"}'
```

---

## `POST /v1/moon/transit`

Транзитная Луна за диапазон (≤ 400 дней, ~13 месяцев — календарный год с запасом
за один вызов): **окна Луны без курса** и **путь Луны через натальные дома**. То же нахождение корней, что у `moon_void`; для пути по
домам нужна натальная карта с местоположением (иначе только `void_windows`).

### Тело запроса (`MoonTransitRequest`)

| Поле | Тип | Обязательный | Примечания |
|---|---|---|---|
| `natal` | `NatalRequest` | да | Местоположение нужно для `house_path` |
| `from`, `to` | string (RFC 3339) | да | Диапазон, UTC; максимум 400 дней (~13 месяцев) |

### Ответ (`MoonTransit`) — 200 OK

| Блок | Форма элемента | Примечания |
|---|---|---|
| `void_windows` | `{ from, to, via_combusta?, spica_exception?, clipped_start?, clipped_end? }` | Каждый период Луны без курса; конец окна = следующая ингрессия. Булевы флаги опускаются, когда false |
| `house_path` | `{ from, to, sign, natal_house }` | Непрерывные сегменты постоянного знака + натального дома (пересечения куспидов по развёрнутой долготе). Пусто без местоположения |

### Пример

```bash
curl -s -X POST https://astro.yuretz.ru/v1/moon/transit \
  -H 'content-type: application/json' \
  -d '{"natal":{"datetime_utc":"1990-05-15T09:20:00Z","location":{"latitude":55.7558,"longitude":37.6178}},
       "from":"2026-07-01T00:00:00Z","to":"2026-07-15T00:00:00Z"}'
```

---

## `GET /v1/extras`

Затмения, ближайшие к заданному моменту, и данные неподвижной звезды Алголь
(β Персея) — панель астрособытий вкладки «Информация». Вычисление разделяется с
WASM-клиентом (`astro_core::extras`), поэтому результаты точно совпадают.

### Параметры запроса

| Параметр | Тип | Обязательный | По умолчанию | Примечания |
|---|---|---|---|---|
| `jd` | number | да | — | Юлианский день (UT) — обычно `julian_day_ut` карты |
| `zodiac` | string | нет | `tropical` | Только `sidereal` что-то меняет (влияет на `algol_longitude`); любое другое значение ведёт себя как `tropical` |

### Ответ (`ExtrasResult`) — 200 OK

| Поле | Тип | Примечания |
|---|---|---|
| `solar_eclipse` | object \| null | Ближайшее солнечное затмение (в любом направлении во времени) |
| `solar_eclipse.datetime_utc` | string | RFC 3339 с числовым смещением (всегда `+00:00`) |
| `solar_eclipse.kind` | string | `total` `annular` `partial` `hybrid` |
| `lunar_eclipse` | object \| null | Та же форма, `kind`: `total` `partial` `penumbral` |
| `algol_minima` | string[] | Временные метки минимумов яркости Алголя в пределах ±2 дней от `jd` |
| `algol_longitude` | number \| null | Эклиптическая долгота Алголя на `jd`, в запрошенном зодиаке |

### Пример

```bash
curl -s "https://astro.yuretz.ru/v1/extras?jd=2448026.888888889&zodiac=tropical"
```

```json
{
  "solar_eclipse": { "datetime_utc": "1990-07-22T03:02:12+00:00", "kind": "total" },
  "lunar_eclipse": { "datetime_utc": "1990-08-06T14:12:21+00:00", "kind": "partial" },
  "algol_minima": ["1990-05-15T14:29:44+00:00"],
  "algol_longitude": 56.0304994657663
}
```

---

## `GET /v1/calendar/global`

**Фон года** — небо само по себе, без натального ввода: ингрессии в знаки,
ретроградные станции и периоды, мунданные (транзит×транзит) аспекты, фазы Луны и
затмения за диапазон дат, в одном объекте. Это единый пакетный проход по эфемеридам,
предназначенный для вычисления **один раз в год и кэширования для всех** (стоимость
на пользователя ≈ 0). Каждый `exact` это истинное пересечение методом нахождения
корней (момент UTC до субсекундной точности), никогда не оценка; разбиение событий
на локальные дни — задача клиента (отправляйте `from`/`to` в UTC с ±1 днём отступа
вокруг целевого местного года).

Луна **по умолчанию исключена** из ингрессий и пар аспектов (её ~146 ингрессий в год
и сотни аспектов — забота на уровне пользователя/подсчёта очков) — назовите `moon`
явно в `bodies`, чтобы её включить.

### Параметры запроса

| Параметр | Тип | Обязательный | По умолчанию | Примечания |
|---|---|---|---|---|
| `from` | string (RFC 3339) | да | — | Начало диапазона, UTC |
| `to` | string (RFC 3339) | да | — | Конец диапазона, UTC; максимум ~2 года после `from` |
| `bodies` | string | нет | `sun,mercury,venus,mars,jupiter,saturn,uranus,neptune,pluto` | Список через запятую. Управляет ингрессиями + станциями. Если задан явно, мунданные аспекты перечисляют **все** пары среди этих тел (включайте Луну/быстрые тела здесь) |
| `aspects` | string | нет | `conjunction,sextile,square,trine,opposition` | Список видов аспектов через запятую для `transit_aspects` |
| `zodiac` | string | нет | `tropical` | `sidereal` сдвигает все долготы |

Когда `bodies` опущено, `transit_aspects` использует кураторский пул пар — социальные
/внешние между собой плюс каждая к Солнцу и Марсу
(`sun,mars,jupiter,saturn,uranus,neptune,pluto`) — чтобы бесплатный слой не был
затоплен шумом быстрых внутренних планет.

### Ответ (`GlobalCalendar`) — 200 OK

| Блок | Форма элемента | Примечания |
|---|---|---|
| `ingresses` | `{ body, sign, from_sign, exact, retrograde }` | Каждое пересечение границы знака; ретроградные повторные пересечения — отдельные записи. Отсортировано по `exact` |
| `stations` | `{ body, kind, exact, longitude, sign }` | `kind`: `retrograde` (D→R) / `direct` (R→D). Без Солнца/Луны/узлов |
| `retrograde_periods` | `{ body, start, end, sign_start, sign_end, clipped_start, clipped_end }` | Спаренные станции; промежуток за краем диапазона обрезается и помечается |
| `transit_aspects` | `{ body_a, body_b, kind, angle, exact, longitude, retrograde_a, retrograde_b }` | Точные мунданные аспекты; пара может стать точной до 3× (ретроградность). Отсортировано по `exact` |
| `moon_phases` | `{ phase, exact, moon_sign, sun_sign, moon_longitude, eclipse? }` | `phase`: `new`/`first_quarter`/`full`/`last_quarter`; `eclipse` = `solar`/`lunar`, если сизигия является затмением |
| `eclipses` | `{ kind, type, exact, longitude, sign, node }` | Полный список в диапазоне (обычно 4–7 в год). `node`: `north`/`south` (для попаданий на натальные узлы) |

### Ошибки

- `422` — `to` не после `from`, диапазон длиннее ~2 лет или неизвестное имя тела/аспекта.
- `400` — отсутствует `from`/`to` или некорректная временная метка.

### Пример

```bash
curl -s "https://astro.yuretz.ru/v1/calendar/global?from=2025-12-31T00:00:00Z&to=2027-01-02T00:00:00Z"
```

```json
{
  "from": "2025-12-31T00:00:00Z",
  "to": "2027-01-02T00:00:00Z",
  "ingresses": [
    { "body": "mercury", "sign": "capricorn", "from_sign": "sagittarius", "exact": "2026-01-01T21:10:42Z", "retrograde": false }
  ],
  "stations": [
    { "body": "uranus", "kind": "direct", "exact": "2026-02-04T02:33:10Z", "longitude": 57.46, "sign": "taurus" }
  ],
  "retrograde_periods": [
    { "body": "jupiter", "start": "2025-12-31T00:00:00Z", "end": "2026-03-11T03:29:51Z", "sign_start": "cancer", "sign_end": "cancer", "clipped_start": true, "clipped_end": false }
  ],
  "transit_aspects": [
    { "body_a": "sun", "body_b": "mars", "kind": "conjunction", "angle": 0.0, "exact": "2026-01-09T11:41:13Z", "longitude": 289.22, "retrograde_a": false, "retrograde_b": false }
  ],
  "moon_phases": [
    { "phase": "full", "exact": "2026-01-03T10:02:55Z", "moon_sign": "cancer", "sun_sign": "capricorn", "moon_longitude": 103.03 }
  ],
  "eclipses": [
    { "kind": "solar", "type": "annular", "exact": "2026-02-17T12:11:53Z", "longitude": 328.8, "sign": "aquarius", "node": "north" },
    { "kind": "lunar", "type": "total", "exact": "2026-03-03T11:33:41Z", "longitude": 162.9, "sign": "virgo", "node": "south" }
  ]
}
```

---

## `POST /v1/geo/utc`

Преобразовать местную гражданскую дату-время в часовом поясе IANA в UTC, учитывая
исторические изменения DST/пояса (например, декретное время СССР, летнее время 5-го
пояса Казахстана 1992 года). Разделяется с WASM-клиентом (`astro_core::tz`) —
идентичные результаты в браузере и на сервере.

### Тело запроса (`UtcReq`)

| Поле | Тип | Обязательный | Примечания |
|---|---|---|---|
| `datetime_local` | string | да | `YYYY-MM-DDTHH:MM` или `YYYY-MM-DDTHH:MM:SS`, без смещения часового пояса |
| `tz` | string | да | Имя часового пояса IANA, например `Europe/Moscow` |

### Ответ (`UtcResult`) — 200 OK

| Поле | Тип | Примечания |
|---|---|---|
| `datetime_utc` | string | `YYYY-MM-DDTHH:MM:SSZ` |
| `offset` | string | `±HH:MM` |
| `offset_seconds` | integer | |
| `abbreviation` | string | например `MSD`, `MSK` — рендеринг смещения от chrono-tz, не всегда общепринятая аббревиатура |

Для местного времени, попадающего в пробел DST (не существует) или неоднозначного
(происходит дважды), используется **самый ранний** валидный момент UTC.

### Ошибки

- `422` — `invalid input: unknown timezone '…'` для нераспознанного `tz`.
- `422` — `invalid input: bad datetime '…': …` для неразбираемого `datetime_local`.
- `422` — `invalid input: local time does not exist in this timezone` — зарезервировано для (в настоящее время практически недостижимого) случая, когда chrono-tz не может разрешить ни один момент.

### Пример

```bash
curl -s -X POST https://astro.yuretz.ru/v1/geo/utc \
  -H 'content-type: application/json' \
  -d '{ "datetime_local": "1990-05-15T12:00", "tz": "Europe/Moscow" }'
```

```json
{ "datetime_utc": "1990-05-15T08:00:00Z", "offset": "+04:00", "offset_seconds": 14400, "abbreviation": "MSD" }
```

---

## `GET /v1/geo/search`

Поиск по подстроке в базе городов GeoNames (234 640 городов, 42 языка) для выбора
места рождения. Совпадения по префиксу ранжируются выше совпадений по подстроке;
ничьи разрешаются по населению (по убыванию).

### Параметры запроса

| Параметр | Тип | Обязательный | По умолчанию | Примечания |
|---|---|---|---|---|
| `q` | string | да | — | Сопоставляется без учёта регистра с именем города на каждом языке (так русский UI может найти «London», набранный по-английски) |
| `limit` | integer | нет | `8` | Ограничено до `[0, 20]` |
| `lang` | string | нет | `ru` | Один из 42 поддерживаемых кодов (см. ниже); нераспознанные коды молча откатываются к `ru` |

### Ответ (`GeoCity[]`) — 200 OK

| Поле | Тип | Примечания |
|---|---|---|
| `name` | string | Локализовано под `lang`; откатывается к английскому, затем к сырому имени GeoNames |
| `country` | string | Локализованное название страны |
| `admin1` | string | Локализованный административный регион первого уровня (штат/область/…); `""`, если неизвестно |
| `lat`, `lon` | number | Градусы |
| `tz` | string | Часовой пояс IANA, с применёнными историческими корректировками (см. [`swisseph-embedded.md`](swisseph-embedded.md) / модуль geo для случая Тургай/Аркалык 1992) |
| `population` | integer | |

Пустое `q` возвращает `[]`.

### Пример

```bash
curl -s "https://astro.yuretz.ru/v1/geo/search?q=Париж&limit=2&lang=ru"
```

```json
[
  { "name": "Париж", "country": "Франция", "admin1": "Иль-де-Франс", "lat": 48.85341, "lon": 2.3488, "tz": "Europe/Paris", "population": 2138551 },
  { "name": "Париж", "country": "США", "admin1": "Техас", "lat": 33.66094, "lon": -95.55551, "tz": "America/Chicago", "population": 24782 }
]
```

---

## `GET /v1/geo/near`

Ближайший город к координате (обратное геокодирование — используется для
«определить моё местоположение»). Линейное сканирование по планарно
аппроксимированному расстоянию (нормально в масштабе выбора города); не истинный
поиск по большому кругу.

### Параметры запроса

| Параметр | Тип | Обязательный | Примечания |
|---|---|---|---|
| `lat`, `lon` | number | да | Градусы |
| `lang` | string | нет (по умолчанию `ru`) | Так же, как `/v1/geo/search` |

### Ответ — 200 OK

Один объект `GeoCity` или JSON `null`, если база данных пуста (на практике никогда —
таблица всегда встроена).

```bash
curl -s "https://astro.yuretz.ru/v1/geo/near?lat=55.7558&lon=37.6178&lang=ru"
```

---

## `GET /v1/geo/tzcity`

Самый населённый город в заданном часовом поясе IANA — запасной вариант
местоположения, когда GPS недоступен, но известен часовой пояс браузера.

### Параметры запроса

| Параметр | Тип | Обязательный | Примечания |
|---|---|---|---|
| `tz` | string | да | Имя часового пояса IANA |
| `lang` | string | нет (по умолчанию `ru`) | Так же, как `/v1/geo/search` |

### Ответ — 200 OK

Один `GeoCity` или JSON `null`, если ни один город в базе данных не несёт эту точную
строку часового пояса (примечание: это сопоставляется с *сырым* полем `tz` GeoNames,
а не с исправленным историческими корректировками, которое `/v1/geo/search`
возвращает в `tz`).

```bash
curl -s "https://astro.yuretz.ru/v1/geo/tzcity?tz=Europe/Moscow&lang=ru"
```

---

## `GET /v1/geo/pack`

Загружаемый пакет городов по каждому языку, который клиентский JavaScript браузера
(`web/geopack.js`) декодирует для запуска выбора города полностью офлайн.
**Не предназначен для общего потребления через API** — это внутренний, двоичный,
версионированный артефакт — но задокументирован здесь для полноты, поскольку это
публичная HTTP-конечная точка.

### Параметры запроса

| Параметр | Тип | Обязательный | Примечания |
|---|---|---|---|
| `lang` | string | да | Один из 42 поддерживаемых кодов; нераспознанные коды откатываются к `ru` |
| `v` | string | нет | Только сброс кэша, игнорируется на стороне сервера (клиент устанавливает его в текущую версию данных) |

### Ответ — 200 OK

Всегда `Content-Encoding: gzip` (каждый современный клиент декодирует его
прозрачно; несжатого варианта нет). Условный GET поддерживается через
`ETag`/`If-None-Match` → `304 Not Modified`.

| Заголовок | Значение |
|---|---|
| `Content-Type` | `text/tab-separated-values; charset=utf-8` |
| `Content-Encoding` | `gzip` |
| `Cache-Control` | `public, max-age=31536000` |
| `ETag` | Хеш содержимого (до gzip) пакета, меняется, когда меняются данные пакета |

**Формат тела (V3, после декодирования gzip)** — столбцовый, отсортированный
географически, варинт-дельты; не стабильный контракт, может меняться без повышения
версии в этом документе (клиент заново выводит всё из тега `#V3` на проводе и
заголовка длины `#S`, поэтому он самоописывающийся):

```
#V3 <lang>
#S <n> <lenTz> <lenAdm> <lenNames> <lenEns> <lenExtra> <lenLat> <lenLon> <lenTzV> <lenPopV> <lenAdmV>
<tz dictionary, \n-joined>
<admin dictionary, "CC.code\tcountry\tregion" lines>
<names, \n-joined, localized to lang>
<English names, \n-joined, "" when identical to the localized name>
<extra search words, \n-joined, space-separated — every other language's
 spelling not already covered by name/English, so offline search matches
 any of the 42 languages regardless of the pack's language>
<zigzag-varint columns: lat delta ×1e5, lon delta ×1e5, tz index, population, admin index>
```

Строки отсортированы по широтной полосе 0.5° затем по долготе, что удерживает
последовательные координатные дельты малыми для варинт-кодирования. См.
`crates/astro-api/src/geo.rs` (`pack()`) для авторитетного генератора и
`web/geopack.js` для эталонного декодера.

```bash
curl -s "https://astro.yuretz.ru/v1/geo/pack?lang=ru" -o pack.gz   # already gzip on the wire
gunzip -c pack.gz | head -c 200
```

---

## Статические ресурсы — `GET /*`

Всё, что не сопоставлено маршрутом выше (включая неизвестные пути), отдаётся из
встроенного бандла фронтенда (одностраничное приложение): `/`, `/app.js`,
`/style.css`, `/engine.js`, `/engine/astro.wasm`, `/geopack.js`,
`/i18n/<lang>.json`, иконки, шрифты, манифест. `index.html` шаблонизируется тремя
значениями, внедряемыми сервером (версия ресурсов, хеш содержимого WASM, версия
гео-данных), и отдаётся с `Cache-Control: no-cache`; каждый другой ресурс
долго кэшируется (`max-age=31536000`) с `ETag` и сбрасывается через запрос `?v=`,
который встраивает `index.html`. `engine/astro.wasm` дополнительно согласует gzip
(`Accept-Encoding`) и сообщает свой несжатый размер через заголовок `x-wasm-size`,
чтобы клиент мог отрисовать точный UI прогресса загрузки. Это деталь реализации
встроенного веб-приложения, а не стабильный API — см.
[`README.md`](../README.md) для архитектуры фронтенда.

---

## Поддерживаемые языки (параметр `lang`)

42 кода, разделяемых между `/v1/geo/search`, `/v1/geo/near`, `/v1/geo/tzcity`,
`/v1/geo/pack` и `LANG_META` фронтенда:

```
ru uk kk en de fr it pl be az uz ka hy es pt nl sv no da fi cs sk ro hu el
bg sr hr tr ar fa he hi bn ur zh ja ko vi th id ms
```

`ar` `fa` `he` `ur` пишутся справа налево во фронтенде; это не влияет на сам API.
Нераспознанный код не является ошибкой — каждая конечная точка, принимающая `lang`,
откатывается к `ru`.
