"""Template recommendation from a drawing set (S02, 2026-10-15): estimate the fit questions from the drawing texts and rank the protected templates by the number of matching answers. Only questions the texts can answer are estimated; the rest stay None ("판단 불가") and are answered by the user. Estimates are proposals with their evidence, never confirmations. Rules were checked on 9 drawing sets only (Ungok P1-P4, Ulsan P1-P2, Ramp-A P1, Okseong P1, Saejae-1 P1); weak ones are marked. """ import re Q_PILE, Q_SOFFIT, Q_BEARING = "foundation", "soffit", "bearings" QUESTIONS = [ (Q_PILE, "기초 형식(말뚝 유무)"), (Q_SOFFIT, "코핑 하면 형상"), (Q_BEARING, "받침 개수"), ] R_RE = re.compile(r"(?:^|[\s(])R\s*=?\s*(\d[\d,]*(?:\.\d+)?)\s*$") def profile(params): """Answers a template's params.json stands for.""" piles = "piles" in params or "pile" in params cp = params.get("coping", {}) if "soffit_radius" in cp: soffit = "곡선" elif cp.get("form") == "y_shaft": soffit = "나팔형" else: soffit = "직선 경사" found = "말뚝만" if ("footing" not in params and piles) else ("기초판+말뚝" if piles else "기초판만") return {Q_PILE: found, Q_SOFFIT: soffit, Q_BEARING: len(params.get("bearing_seats", {}).get("bearings", []))} def _r_mm(text): m = R_RE.search(text.strip()) if not m: return None s = m.group(1) return round(float(s) * 1000) if re.fullmatch(r"\d+\.\d{3}", s) else round(float(s.replace(",", ""))) def _where(t): return f"{t.get('dwg', '?')} {t.get('layer', '')} ({t['x']:.0f}, {t['y']:.0f}) '{t['text'][:40]}'" def count_bearings(rows): """Largest run 1..n of integer first cells under the ELEVATION TABLE title.""" n = 0 for r in rows: c = r["cells"][0]["text"].strip() if r.get("cells") else "" if c.isdigit() and int(c) == n + 1: n += 1 return n def estimate(texts, table_rows=None): """{question: (answer or None, evidence str, confidence)}; table_rows = rows of the ELEVATION TABLE (reader.table_candidates).""" out = {} piles = [t for t in texts if "말뚝" in t["text"]] cast = [t for t in piles if "현장타설말뚝" in t["text"]] if cast: out[Q_PILE] = ("말뚝만", "현장타설말뚝 문자: " + _where(cast[0]), "약함") elif piles: out[Q_PILE] = ("기초판+말뚝", f"'말뚝' 문자 {len(piles)}개: " + _where(piles[0]), "보통") else: out[Q_PILE] = ("기초판만", "'말뚝' 문자 없음", "보통") rs = [(v, t) for t in texts if len(t["text"]) < 30 and (v := _r_mm(t["text"])) is not None] big = [(v, t) for v, t in rs if v >= 10000] mid = [(v, t) for v, t in rs if 1000 < v < 10000] if big: out[Q_SOFFIT] = ("곡선", f"R {big[0][0]} mm: " + _where(big[0][1]), "보통") elif mid: out[Q_SOFFIT] = ("나팔형", f"R {mid[0][0]} mm: " + _where(mid[0][1]), "약함") else: small = f"(R {rs[0][0]} mm는 평면 모서리 등으로 보고 제외)" if rs else "" out[Q_SOFFIT] = ("직선 경사", "정면도 R 문자 없음 " + small, "약함") n = count_bearings(table_rows) if table_rows else 0 out[Q_BEARING] = (n, f"ELEVATION TABLE 행 {n}개", "보통") if n else (None, "ELEVATION TABLE을 찾지 못함", "") return out def rank(est, profiles): """Templates ordered by the number of estimated answers they match: [{name, matched, asked, hits{q: bool}}].""" asked = [q for q, _ in QUESTIONS if est.get(q, (None,))[0] is not None] rows = [] for name, prof in profiles.items(): hits = {q: prof[q] == est[q][0] for q in asked} rows.append({"name": name, "matched": sum(hits.values()), "asked": len(asked), "hits": hits, "profile": prof}) rows.sort(key=lambda r: -r["matched"]) return rows def mismatches(answers, prof): """Questions whose answer differs from the template (strings compared; 'rebar_*' = '아니오' counts); empty = same form.""" out = [label for q, label in QUESTIONS if str(prof[q]) != str(answers.get(q, "")).strip()] out += [label for key, label in (("rebar_shape", "철근 형상 종류"), ("rebar_method", "철근 생성 방식")) if answers.get(key) == "아니오"] return out