PR-B (follow-up к unify-concept). Закрывает «вписать пятно» из round-3:
- BuildingProgramItem получает опциональные footprint_w_m/footprint_d_m
(float|None, ge=4 le=120, default None → каталог; ADDITIVE, byte-for-byte
backward-compat). place_program кладёт ручное пятно когда заданы ОБА габарита,
иначе каталожное. api-types.ts регенерирован под новую схему.
- ConceptProgramForm: пятно секции теперь редактируемое (ширина × глубина);
пусто → возврат к каталогу; Σ пятно и ориентир КСИТ считаются от эффективного
габарита. building_program несёт footprint в Section7Concept.
- ConceptVariantsResult: грамматика «корпус» (1 корпус / 2–4 корпуса / 5+
корпусов) в headline / подписи карты / partial-fit; program-вариант получает
свой хинт вместо STRATEGY_HINTS[balanced].
- Тесты place_program: footprint-override и partial-footprint fallback.
Extend the concept generator so ConceptInput can carry an optional
building_program (list of typed houses from a catalog). When present,
placement lays out EXACTLY that program — for each item, place `count`
sections of the catalog footprint at the item's floors — instead of the
greedy max-FAR coverage-cap sweep. When absent, the existing greedy
behavior is unchanged (byte-for-byte backward-compatible).
- catalog.py: hardcoded HOUSE_TYPES (panel_econom, monolith_comfort,
tower_business, lowrise_comfort, townhouse) — sane-default catalog,
promote to DB later; get_house_type / available_section_types lookups.
- schema: additive BuildingProgramItem {section_type, floors, count} and
ConceptInput.building_program (default None -> greedy). ConceptVariant
gains optional placed_count / requested_count (partial-fit signal).
- placement: shared _Placer (collision/STRtree/setback machine extracted
from greedy sweep, reused — no duplication); place_program +
place_program_variant; branch in place_all_strategies on
building_program. Mixed-floor TEAP via exact per-floor-group aggregation
(GFA = sum(area_i * floors_i), no rounding drift).
- partial fit: when the parcel can't fit all sections, place as many as
fit and report placed_count < requested_count (no hard-422); zero-fit
still raises ParcelGeometryError (-> 422).
- API: validate program section_type keys against the catalog (unknown ->
422) before placement.
- tests: catalog integrity, greedy backward-compat, exact 2-item program +
TEAP reflection, over-packed partial placement, API program path.
- regenerate frontend api-types.ts (OpenAPI codegen gate stays green).