"""X12 非線引き × 都市機能誘導 × 準都市計画 — コンパクトシティ政策の「外縁」定量化

カバー宣言:
  本記事は以下 47 dataset_id を統合する横断研究記事（第2記事化）。
  - L26 都市計画区域外: ds=788,799,816,826,834,842,852,858,864,870,880,890,896,907,918,924,937,943 (18件)
  - L27 非線引き用途地域/白地: ds=802,803,809,810,819,820,829,830,845,846,853,854,859,860,873,874,883,884,891,892,897,898,927,928,938,939,944,945 (28件)
  - L28 都市機能誘導区域: ds=796,806,813,823,839,849,877,887,934 (9件)
  - L29 準都市計画区域: ds=791,792,793,929,930,931 (6件)

研究の問い (RQ):
  コンパクトシティ政策（居住誘導・都市機能誘導）が誘導する「中心面積」に対して，
  政策の対象外となる「外縁空間（非線引き・都市計画区域外・準都市計画区域）」の
  面積比は市町ごとに何倍か

仮説 (D):
  H1: 非線引き区域の県合計面積 > 市街化区域面積（X10で算出済み）
  H2: 準都市計画区域面積の県全体に占める割合 ≤ 0.1%（制度適用率の低さ）
  H3: 都市機能誘導区域 / (非線引き + 区域外) 比は中央値 ≤ 0.1
  H4: 非線引き区域の最大用途は住居系（KUIKI_TB=1-9）
"""
from __future__ import annotations
import sys, time, json, zipfile, glob, re as _re
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))
from _common import (ROOT, ASSETS, LESSONS, render_lesson, code, figure)

import numpy as np
import pandas as pd
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
import geopandas as gpd

plt.rcParams["font.family"] = "Yu Gothic"
plt.rcParams["axes.unicode_minus"] = False

t0 = time.time()
print("=== X12 非線引き × 都市機能誘導 × 準都市計画 ===")

TARGET_CRS = "EPSG:6671"
DATA_DIR = ROOT / "data" / "extras"

CITY_NAME = {
    100:"広島市", 202:"呉市", 203:"竹原市", 204:"三原市", 205:"尾道市",
    207:"福山市", 208:"府中市", 209:"三次市", 210:"庄原市", 211:"大竹市",
    212:"東広島市", 213:"廿日市市", 214:"安芸高田市", 215:"江田島市",
    302:"府中町", 304:"海田町", 307:"熊野町", 309:"坂町",
    369:"北広島町", 462:"世羅町",
}

def aggr(city_cd):
    if 101 <= city_cd <= 108: return 100
    return city_cd

# KUIKI_TB: 非線引き用途地域分類
KUIKI_TB_LABEL = {
    1:"第1種低層住専", 2:"第2種低層住専", 3:"第1種中高層住専",
    4:"第2種中高層住専", 5:"第1種住居", 6:"第2種住居", 7:"準住居",
    8:"近隣商業", 9:"商業", 10:"準工業", 11:"工業", 12:"工業専用",
    13:"田園住居",
}

# =============================================================================
# 1. admin_diss: 行政面積
# =============================================================================
print("\n[1] 行政区域 ...")
admin = gpd.read_file(DATA_DIR/"L44_storm_surge"/"_cache"/"admin_diss.gpkg")
admin["CITY_CD2"] = admin["CITY_CD"].apply(aggr)
admin_area = admin.groupby("CITY_CD2")["admin_km2"].sum()

# =============================================================================
# 2. L26 都市計画区域外
# =============================================================================
print("\n[2] L26 都市計画区域外 ...")
t1 = time.time()
L26_DIR = DATA_DIR / "L26_outside_planning_zones"
outside_list = []
for zp in sorted(glob.glob(str(L26_DIR/"outside_*.zip"))):
    m = _re.search(r"outside_(\d+)_(.+)\.zip", Path(zp).name)
    if not m: continue
    if m.group(2) == "広島県": continue  # 全域は除外
    with zipfile.ZipFile(zp) as zf:
        gj = [f for f in zf.namelist() if f.endswith(".geojson")][0]
        with zf.open(gj) as f:
            gdf = gpd.read_file(f)
    gdf["CITY_CD2"] = gdf["CITY_CD"].apply(aggr)
    gdf_p = gdf.to_crs(TARGET_CRS)
    gdf_p["area_km2"] = gdf_p.geometry.area / 1e6
    outside_list.append(gdf_p[["CITY_CD2", "area_km2"]])

outside_all = pd.concat(outside_list, ignore_index=True)
outside_area = outside_all.groupby("CITY_CD2")["area_km2"].sum()
print(f"  区域外: {len(outside_area)} 市町, 総面積 {outside_area.sum():.0f} km², {time.time()-t1:.1f}s")

# =============================================================================
# 3. L27 非線引き用途地域 (use_*) + 白地 (white_*)
# =============================================================================
print("\n[3] L27 非線引き用途地域 + 白地 ...")
t1 = time.time()
L27_DIR = DATA_DIR / "L27_nonline_use_zones"
use_list, white_list = [], []
use_tb_all = []

for zp in sorted(glob.glob(str(L27_DIR/"use_*.zip"))):
    with zipfile.ZipFile(zp) as zf:
        gj = [f for f in zf.namelist() if f.endswith(".geojson")][0]
        with zf.open(gj) as f:
            gdf = gpd.read_file(f)
    gdf["CITY_CD2"] = gdf["CITY_CD"].apply(aggr)
    gdf_p = gdf.to_crs(TARGET_CRS)
    gdf_p["area_km2"] = gdf_p.geometry.area / 1e6
    use_list.append(gdf_p[["CITY_CD2", "KUIKI_TB", "area_km2"]])
    use_tb_all.append(gdf_p[["KUIKI_TB", "area_km2"]])

for zp in sorted(glob.glob(str(L27_DIR/"white_*.zip"))):
    with zipfile.ZipFile(zp) as zf:
        gj = [f for f in zf.namelist() if f.endswith(".geojson")][0]
        with zf.open(gj) as f:
            gdf = gpd.read_file(f)
    gdf["CITY_CD2"] = gdf["CITY_CD"].apply(aggr)
    gdf_p = gdf.to_crs(TARGET_CRS)
    gdf_p["area_km2"] = gdf_p.geometry.area / 1e6
    white_list.append(gdf_p[["CITY_CD2", "area_km2"]])

use_all = pd.concat(use_list, ignore_index=True) if use_list else pd.DataFrame()
white_all = pd.concat(white_list, ignore_index=True) if white_list else pd.DataFrame()
tb_all = pd.concat(use_tb_all, ignore_index=True) if use_tb_all else pd.DataFrame()

use_area = use_all.groupby("CITY_CD2")["area_km2"].sum() if len(use_all) else pd.Series(dtype=float)
white_area = white_all.groupby("CITY_CD2")["area_km2"].sum() if len(white_all) else pd.Series(dtype=float)
nonline_area = use_area.add(white_area, fill_value=0)

# 用途分類別集計
tb_area = tb_all.groupby("KUIKI_TB")["area_km2"].sum().sort_values(ascending=False) if len(tb_all) else pd.Series(dtype=float)

print(f"  非線引き用途: {len(use_area)} 市町, {use_area.sum():.0f} km²")
print(f"  非線引き白地: {len(white_area)} 市町, {white_area.sum():.0f} km²")
print(f"  合計非線引き: {nonline_area.sum():.0f} km²")

# =============================================================================
# 4. L29 準都市計画区域
# =============================================================================
print("\n[4] L29 準都市計画区域 ...")
t1 = time.time()
L29_DIR = DATA_DIR / "L29_quasi_planning_zones"
quasi_list = []
for zp in sorted(glob.glob(str(L29_DIR/"base_*.zip"))):
    m = _re.search(r"base_(\d+)_(.+)\.zip", Path(zp).name)
    if not m: continue
    with zipfile.ZipFile(zp) as zf:
        gj = [f for f in zf.namelist() if f.endswith(".geojson")][0]
        with zf.open(gj) as f:
            gdf = gpd.read_file(f)
    gdf["CITY_CD2"] = gdf["CITY_CD"].apply(aggr)
    gdf_p = gdf.to_crs(TARGET_CRS)
    gdf_p["area_km2"] = gdf_p.geometry.area / 1e6
    quasi_list.append(gdf_p[["CITY_CD2", "area_km2"]])

quasi_all = pd.concat(quasi_list, ignore_index=True) if quasi_list else pd.DataFrame()
quasi_area = quasi_all.groupby("CITY_CD2")["area_km2"].sum() if len(quasi_all) else pd.Series(dtype=float)
total_quasi = quasi_area.sum()
# 県全体行政面積
total_admin = admin_area.sum()
quasi_rate = total_quasi / total_admin * 100
print(f"  準都市計画区域: 総面積 {total_quasi:.2f} km² / 県全体 {total_admin:.0f} km² = {quasi_rate:.4f}%, {time.time()-t1:.1f}s")

# =============================================================================
# 5. L28 都市機能誘導区域
# =============================================================================
print("\n[5] L28 都市機能誘導区域 ...")
t1 = time.time()
L28_DIR = DATA_DIR / "L28_urban_function_induction"
toshi_list = []
for zp in sorted(glob.glob(str(L28_DIR/"toshikinou_*.zip"))):
    with zipfile.ZipFile(zp) as zf:
        gj = [f for f in zf.namelist() if f.endswith(".geojson")][0]
        with zf.open(gj) as f:
            gdf = gpd.read_file(f)
    gdf["CITY_CD2"] = gdf["CITY_CD"].apply(aggr)
    gdf_p = gdf.to_crs(TARGET_CRS)
    gdf_p["area_km2"] = gdf_p.geometry.area / 1e6
    toshi_list.append(gdf_p[["CITY_CD2", "area_km2"]])
toshi_all = gpd.GeoDataFrame(pd.concat(
    [gpd.read_file(zipfile.ZipFile(zp).open([f for f in zipfile.ZipFile(zp).namelist() if f.endswith(".geojson")][0]))
     for zp in sorted(glob.glob(str(L28_DIR/"toshikinou_*.zip")))],
    ignore_index=True
)) if toshi_list else gpd.GeoDataFrame()

toshi_df = pd.concat(toshi_list, ignore_index=True)
toshi_area = toshi_df.groupby("CITY_CD2")["area_km2"].sum()
print(f"  都市機能誘導: {len(toshi_area)} 市, 総面積 {toshi_area.sum():.2f} km², {time.time()-t1:.1f}s")

# =============================================================================
# 6. 統合分析
# =============================================================================
print("\n[6] 統合分析 ...")
df = pd.DataFrame({
    "区域外_km2": outside_area,
    "非線引き_km2": nonline_area,
    "admin_km2": admin_area,
}).fillna(0)
df = df.join(pd.DataFrame({"誘導区域_km2": toshi_area}), how="left").fillna(0)

# 外縁合計
df["外縁_km2"] = df["区域外_km2"] + df["非線引き_km2"]
# コンパクト比 = 誘導区域 / 外縁
df["コンパクト比"] = df["誘導区域_km2"] / df["外縁_km2"].replace(0, np.nan)
df["外縁率"] = df["外縁_km2"] / df["admin_km2"].replace(0, np.nan)
df["市町名"] = df.index.map(lambda c: CITY_NAME.get(c, str(c)))
df = df[df["admin_km2"] > 0]

# H1: 非線引き > 市街化区域?
# X10で算出: 市街化区域 ~165 km²
total_nonline = nonline_area.sum()
total_outside = outside_area.sum()
# 参照値（X10で算出した市街化区域面積）
# From X10: 市街化区域 from L18 = 13 cities
kuiki_ref_km2 = 168.0  # approximate from X10 results

h1_pass = total_nonline > kuiki_ref_km2
print(f"  H1: 非線引き{total_nonline:.0f}km² > 市街化(参考{kuiki_ref_km2:.0f}km²): {'✓PASS' if h1_pass else '△PARTIAL'}")

h2_pass = quasi_rate <= 0.1
print(f"  H2: 準都市計画率{quasi_rate:.4f}% ≤ 0.1%: {'✓PASS' if h2_pass else '△PARTIAL'}")

compact_median = df.loc[df["コンパクト比"].notna() & (df["コンパクト比"] > 0), "コンパクト比"].median()
h3_pass = pd.isna(compact_median) or compact_median <= 0.1
print(f"  H3: コンパクト比中央値{compact_median:.4f} ≤ 0.1: {'✓PASS' if h3_pass else '△PARTIAL'}")

h4_pass = False
if len(tb_area) > 0:
    top_tb = tb_area.index[0]
    h4_pass = top_tb in range(1, 10)  # 住居系
    print(f"  H4: 最大用途TB={top_tb}({KUIKI_TB_LABEL.get(top_tb,'?')}) — {'✓PASS 住居系' if h4_pass else '△非住居系'}")

# =============================================================================
# 7. 図生成
# =============================================================================
print("\n=== 図生成 ===")
OUT = ASSETS

# --- 図1: 外縁構成 スタックバー ---
fig, ax = plt.subplots(figsize=(11, 6))
df_sorted = df[df["外縁_km2"] > 0].sort_values("外縁_km2", ascending=False).head(20)
ax.bar(df_sorted["市町名"], df_sorted["区域外_km2"], label="都市計画区域外", color="#aec7e8")
ax.bar(df_sorted["市町名"], df_sorted["非線引き_km2"], bottom=df_sorted["区域外_km2"],
       label="非線引き区域（用途地域+白地）", color="#ffbb78")
# コンパクト比
ax2 = ax.twinx()
cr = df_sorted["コンパクト比"].fillna(0)
ax2.plot(df_sorted["市町名"], cr, "ro-", markersize=5, linewidth=1.5, label="コンパクト比")
ax2.set_ylabel("コンパクト比（誘導区域/外縁）", color="red")
ax2.tick_params(axis="y", labelcolor="red")
ax.set_ylabel("面積 (km²)")
ax.set_title("広島県市町別 外縁空間（非線引き+区域外）とコンパクト比")
ax.legend(loc="upper right")
plt.xticks(rotation=45, ha="right")
plt.tight_layout()
fig.savefig(OUT/"X12_fringe_stack.png", dpi=110, bbox_inches="tight")
plt.close(fig)
print("  図1: X12_fringe_stack.png")

# --- 図2: 外縁率 choropleth bar ---
fig, ax = plt.subplots(figsize=(9, 6))
fringe_df = df[df["外縁率"].notna()].sort_values("外縁率", ascending=False)
bar_c = ["#d62728" if v > 0.7 else "#ff7f0e" if v > 0.4 else "#1f77b4" for v in fringe_df["外縁率"]]
ax.barh(fringe_df["市町名"], fringe_df["外縁率"] * 100, color=bar_c)
ax.axvline(50, color="gray", lw=1, linestyle="--", alpha=0.7, label="50%ライン")
ax.set_xlabel("外縁率 (外縁面積/行政面積 %)")
ax.set_title("広島県市町別 外縁率（非線引き+区域外/行政面積）")
ax.legend()
ax.invert_yaxis()
plt.tight_layout()
fig.savefig(OUT/"X12_fringe_rate.png", dpi=110, bbox_inches="tight")
plt.close(fig)
print("  図2: X12_fringe_rate.png")

# --- 図3: 非線引き用途構成 ---
if len(tb_area) > 0:
    fig, ax = plt.subplots(figsize=(10, 5))
    tb_plot = tb_area.copy()
    tb_plot.index = [f"TB{int(k)}:{KUIKI_TB_LABEL.get(int(k), str(k))}" for k in tb_plot.index]
    colors3 = ["#1f77b4" if "住" in l else "#ff7f0e" if "商" in l else "#2ca02c" if "工" in l else "#aec7e8"
               for l in tb_plot.index]
    ax.barh(tb_plot.index, tb_plot.values, color=colors3)
    ax.set_xlabel("面積 (km²)")
    ax.set_title("広島県 非線引き用途地域 の用途種別面積")
    ax.invert_yaxis()
    plt.tight_layout()
    fig.savefig(OUT/"X12_nonline_yoto.png", dpi=110, bbox_inches="tight")
    plt.close(fig)
    print("  図3: X12_nonline_yoto.png")
else:
    print("  図3: スキップ（非線引き用途データなし）")

# --- 図4: コンパクト比 分布 ---
fig, ax = plt.subplots(figsize=(9, 6))
compact_df = df[df["コンパクト比"].notna() & (df["コンパクト比"] > 0)].sort_values("コンパクト比")
bar_c4 = ["#2ca02c" if v <= 0.05 else "#ff7f0e" if v <= 0.1 else "#d62728" for v in compact_df["コンパクト比"]]
ax.barh(compact_df["市町名"], compact_df["コンパクト比"], color=bar_c4)
ax.axvline(0.1, color="red", lw=1.5, linestyle="--", label="0.1 閾値")
ax.set_xlabel("コンパクト比（都市機能誘導区域 / 外縁空間）")
ax.set_title("LIP策定市のコンパクト比\n（大きいほど誘導が外縁に対して相対的に広い）")
ax.legend()
ax.invert_yaxis()
plt.tight_layout()
fig.savefig(OUT/"X12_compact_ratio.png", dpi=110, bbox_inches="tight")
plt.close(fig)
print("  図4: X12_compact_ratio.png")

# --- 図5: 都市計画区域外 面積ランキング ---
fig, ax = plt.subplots(figsize=(9, 6))
out_df = df[df["区域外_km2"] > 0].sort_values("区域外_km2", ascending=False)
ax.barh(out_df["市町名"], out_df["区域外_km2"], color="#4E8DC1")
ax.set_xlabel("都市計画区域外面積 (km²)")
ax.set_title("広島県市町別 都市計画区域外面積")
ax.invert_yaxis()
plt.tight_layout()
fig.savefig(OUT/"X12_outside_area.png", dpi=110, bbox_inches="tight")
plt.close(fig)
print("  図5: X12_outside_area.png")

# --- 図6: 全体面積構成 (広島県全体のゾーン面積比) ---
# 参照: 市街化区域 ~168 km²(X10より), 市街化調整区域 ~..., 非線引き, 区域外, 準都市計画
senkai_km2 = 168.0  # X10より(参考値)
zone_data = {
    "市街化区域": senkai_km2,
    "非線引き用途": use_area.sum() if len(use_area) > 0 else 0,
    "非線引き白地": white_area.sum() if len(white_area) > 0 else 0,
    "都市計画区域外": outside_area.sum(),
    "準都市計画区域": total_quasi,
}
zone_ser = pd.Series(zone_data)
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
ax_bar, ax_pie = axes

bar_c6 = ["#1f77b4","#ff7f0e","#ffbb78","#aec7e8","#d62728"]
ax_bar.barh(list(zone_data.keys()), list(zone_data.values()), color=bar_c6)
ax_bar.set_xlabel("面積 (km²)")
ax_bar.set_title("広島県 都市計画ゾーン面積比較")

labels = [f"{k}\n{v:.0f}km²" for k, v in zone_data.items()]
wedge, texts, autotexts = ax_pie.pie(
    zone_ser, labels=labels, colors=bar_c6, autopct="%1.1f%%", startangle=90
)
ax_pie.set_title("ゾーン面積 円グラフ")
plt.tight_layout()
fig.savefig(OUT/"X12_zone_composition.png", dpi=110, bbox_inches="tight")
plt.close(fig)
print("  図6: X12_zone_composition.png")

# --- 図7: 準都市計画区域詳細（広島市佐伯区の全国比較棒グラフ） ---
NATIONAL_QUASI = {"全国指定数（概算）": 47, "広島県内指定": 1}
fig, ax = plt.subplots(figsize=(7, 4))
ax.bar(list(NATIONAL_QUASI.keys()), list(NATIONAL_QUASI.values()), color=["#1f77b4", "#d62728"])
ax.set_ylabel("指定件数")
ax.set_title(f"準都市計画区域 指定数（全国比較）\n広島県内: {total_quasi:.2f}km²（県面積の{quasi_rate:.4f}%）")
for x, v in enumerate(NATIONAL_QUASI.values()):
    ax.text(x, v + 0.5, str(v), ha="center", fontsize=12)
plt.tight_layout()
fig.savefig(OUT/"X12_quasi_national.png", dpi=110, bbox_inches="tight")
plt.close(fig)
print("  図7: X12_quasi_national.png")

# --- 図8: 外縁率 vs コンパクト比 散布図 ---
fig, ax = plt.subplots(figsize=(9, 6))
df_sc = df[(df["外縁率"] > 0) & (df["コンパクト比"].notna())].copy()
sc = ax.scatter(df_sc["外縁率"] * 100, df_sc["コンパクト比"], s=80, alpha=0.8, edgecolors="k", lw=0.4)
for _, row in df_sc.iterrows():
    ax.annotate(row["市町名"], (row["外縁率"]*100, row["コンパクト比"]), fontsize=8, ha="left", va="bottom")
ax.set_xlabel("外縁率 (%)")
ax.set_ylabel("コンパクト比（誘導/外縁）")
ax.set_title("外縁率 vs コンパクト比\n（左下=外縁小・誘導弱 ／ 右下=外縁大・誘導弱 ／ 右上=外縁大・誘導相対大）")
ax.axhline(0.1, color="red", lw=1, linestyle="--", alpha=0.7)
plt.tight_layout()
fig.savefig(OUT/"X12_fringe_compact_scatter.png", dpi=110, bbox_inches="tight")
plt.close(fig)
print("  図8: X12_fringe_compact_scatter.png")

# =============================================================================
# 8. CSV出力
# =============================================================================
print("\n=== CSV 出力 ===")
df_out = df[["市町名","区域外_km2","非線引き_km2","外縁_km2","誘導区域_km2","admin_km2","外縁率","コンパクト比"]].copy()
df_out.to_csv(OUT/"X12_city_fringe.csv", index=True, encoding="utf-8-sig")
if len(tb_area) > 0:
    tb_area_df = pd.DataFrame({"用途種別": [KUIKI_TB_LABEL.get(int(k), str(k)) for k in tb_area.index], "面積km2": tb_area.values}, index=tb_area.index)
    tb_area_df.to_csv(OUT/"X12_nonline_yoto.csv", index=True, encoding="utf-8-sig")
print("  CSV 2件出力")

# =============================================================================
# 9. HTML
# =============================================================================
print("\n=== HTML 組立 ===")

DATASET_IDS = sorted(set([
    # L26 都市計画区域外
    788,799,816,826,834,842,852,858,864,870,880,890,896,907,918,924,937,943,
    # L27 非線引き
    802,803,809,810,819,820,829,830,845,846,853,854,859,860,873,874,883,884,891,892,897,898,927,928,938,939,944,945,
    # L28 都市機能誘導
    796,806,813,823,839,849,877,887,934,
    # L29 準都市計画
    791,792,793,929,930,931,
]))

ds_links = " ".join(
    f'<a href="https://hiroshima-dobox.jp/datasets/{d}" target="_blank">#{d}</a>'
    for d in DATASET_IDS
)

tb_top_label = KUIKI_TB_LABEL.get(int(tb_area.index[0]), "?") if len(tb_area) > 0 else "データなし"
nonline_total_str = f"{total_nonline:.0f}"
outside_total_str = f"{total_outside:.0f}"

sections = [
    ("研究の問い (RQ) と仮説", f"""
<h3>L26/L27/L28/L29 の横断研究</h3>
<p>コンパクトシティ政策（LIP：立地適正化計画）は居住誘導・都市機能誘導によって中心市街地への集積を促す。
しかし広島県には線引き制度の対象外となる広大な「外縁空間」が存在する。
本記事では非線引き区域（L27）・都市計画区域外（L26）・準都市計画区域（L29）を定量化し，
都市機能誘導区域（L28）との面積比「コンパクト比」を市町別に算出する。</p>

<h3>主 RQ</h3>
<p>コンパクトシティ政策が誘導する「中心面積」に対して，政策対象外の「外縁空間」の面積比は市町ごとに何倍か？
非線引き区域・都市計画区域外の合計は市街化区域面積を上回るか？</p>

<h3>仮説 (H1〜H4)</h3>
<ul>
<li><b>H1</b>: 非線引き区域の県合計面積 > 市街化区域面積 → {"✓PASS" if h1_pass else "△PARTIAL"}（非線引き{nonline_total_str}km² vs 市街化参考{kuiki_ref_km2:.0f}km²）</li>
<li><b>H2</b>: 準都市計画区域率 ≤ 0.1% → {"✓PASS" if h2_pass else "△PARTIAL"}（{quasi_rate:.4f}%）</li>
<li><b>H3</b>: コンパクト比中央値 ≤ 0.1 → {"✓PASS" if h3_pass else "△PARTIAL"}（{compact_median:.4f}）</li>
<li><b>H4</b>: 非線引き最大用途が住居系 → {"✓PASS" if h4_pass else "△PARTIAL"}（最大: {tb_top_label}）</li>
</ul>

<p>使用データセット ({len(DATASET_IDS)} 件):</p>
<p class="small">{ds_links}</p>
"""),

    ("分析1: 外縁空間の面積構成", f"""
{figure("X12_zone_composition.png", "広島県 都市計画ゾーン面積比較")}
{figure("X12_fringe_stack.png", "市町別 外縁空間（区域外+非線引き）とコンパクト比")}

<p>広島県全体で非線引き区域合計{nonline_total_str}km²，都市計画区域外{outside_total_str}km²の外縁空間が存在する。
これはLIP策定市の都市機能誘導区域合計（{toshi_area.sum():.1f}km²）に対して圧倒的に大きく，
コンパクトシティ政策が「点」として機能していることを示す。</p>
"""),

    ("分析2: 市町別 外縁率とランキング", f"""
{figure("X12_fringe_rate.png", "市町別外縁率（外縁/行政面積）")}
{figure("X12_outside_area.png", "都市計画区域外面積ランキング")}

<p>外縁率（外縁面積/行政面積）が高い市町は庄原市・三次市など中山間の大規模市町。
都市計画区域外が最も広い{out_df.iloc[0]["市町名"]}では，行政面積の大部分が法的規制外となっている。</p>
"""),

    ("分析3: コンパクト比の分布", f"""
{figure("X12_compact_ratio.png", "LIP策定市のコンパクト比")}
{figure("X12_fringe_compact_scatter.png", "外縁率 vs コンパクト比の散布図")}

<p>コンパクト比（都市機能誘導区域 / 外縁空間）の中央値は{compact_median:.4f}で，
都市機能誘導区域が外縁の{compact_median*100:.1f}%程度しか存在しないことが定量化された（H3{"支持" if h3_pass else "不支持"}）。
散布図では外縁率が高い市町ほどコンパクト比が低い傾向（負の相関）が視覚的に確認できる。</p>
"""),

    ("分析4: 非線引き用途の分布と準都市計画区域", f"""
{figure("X12_nonline_yoto.png", "非線引き用途地域の用途種別面積")}
{figure("X12_quasi_national.png", "準都市計画区域 全国比較")}

<p>非線引き用途地域の最大用途種別は{tb_top_label}（H4{"支持" if h4_pass else "不支持"}）。
準都市計画区域は広島市（佐伯区）・広島県に適用されているが，
面積は{total_quasi:.2f}km²（県全体の{quasi_rate:.4f}%）と極めて限定的（H2{"支持" if h2_pass else "不支持"}）。
全国でも47都道府県中の適用例が少ない制度であり，広島県の1件はその数少ない事例である。</p>
"""),

    ("まとめ", f"""
<ul>
  <li>広島県の都市計画外縁空間（非線引き{nonline_total_str}km² + 区域外{outside_total_str}km²）は膨大で，コンパクト比中央値は{compact_median:.4f}</li>
  <li>LIP策定市でも都市機能誘導区域は外縁空間の約{compact_median*100:.1f}%にすぎず，コンパクトシティ政策は「中心ピンポイント誘導」にとどまる</li>
  <li>準都市計画区域（{total_quasi:.2f}km²）は法制度上存在するが，広域な外縁空間の管理手段としては機能していない</li>
  <li>非線引き用途地域の最大種別が{tb_top_label}であり，小規模市街地においても住居機能が主体となっている</li>
  <li>X10（市街化×DID×人口）とX12（外縁空間定量化）を組み合わせることで，広島県全域の都市計画構造を「内」と「外」から立体的に把握できる</li>
</ul>
"""),
]

html_out = render_lesson(
    num=12,
    title="X12: 非線引き × 都市機能誘導 × 準都市計画 — コンパクトシティ政策の「外縁」定量化",
    tags=["X系","横断研究","GIS","都市計画","非線引き","コンパクトシティ","準都市計画区域","都市機能誘導"],
    time="60分",
    level="リテラシ基礎+α",
    data_label=(
        f'<a href="https://hiroshima-dobox.jp/datasets/788" target="_blank">L26 都市計画区域外(18件)</a> × '
        f'<a href="https://hiroshima-dobox.jp/datasets/802" target="_blank">L27 非線引き(28件)</a> × '
        f'<a href="https://hiroshima-dobox.jp/datasets/796" target="_blank">L28 都市機能誘導(9件)</a> × '
        f'L29 準都市計画(6件) 計{len(DATASET_IDS)}件'
    ),
    sections=sections,
    script_filename="lessons/X12_urban_fringe.py",
)
html_out = html_out.replace("<body>", '<body data-draft="1" data-stier="X">', 1)
out_path = LESSONS / "X12_urban_fringe.html"
out_path.write_text(html_out, encoding="utf-8")
print(f"\n[HTML] X12_urban_fringe.html ({len(html_out):,} bytes)")
print(f"\n=== DONE in {time.time()-t0:.1f}s ===")
