mecab-ko-core 0.7.1

한국어 형태소 분석 핵심 엔진 - Lattice, Viterbi, 토크나이저
Documentation
use crate::sejong::types::SejongToken;

/// 192~202차: 복합명사/형태소 병합·분리
///
/// - 192차: "가/VV + 지/NNB + 고/EC" → "가지/VV + 고/EC" 병합
/// - 196차: XPN 복합어 분리 (맨손 → 맨/XPN 손/NNG)
/// - 200차: "밤낮/NNG" → "밤/NNG 낮/NNG" 분리
/// - 197차: "작은집" 복합 접두사 분리
/// - 202차: 복합명사 병합 (여론+조사 → 여론조사)
/// - 198차: "높이/NNG" → "높/VA 이/EC" 분리
/// - 199차: VA 어간 목록 (낮/NNG + 이/JKS 패턴)
/// - 190차: "VV + 히다/NNP" → "VV + 히/VX + 다/EF"
pub(super) fn apply_compound_noun_corrections(tokens: &mut Vec<SejongToken>) {
    // 192차: "가/VV + 지/NNB + 고/EC" → "가지/VV + 고/EC" 병합
    // "가지고 오다" = "가지/VV 고/EC 오/VV 다/EF"
    let mut i = 0;
    while i + 2 < tokens.len() {
        if tokens[i].surface == ""
            && tokens[i].pos == "VV"
            && tokens[i + 1].surface == ""
            && (tokens[i + 1].pos == "NNB" || tokens[i + 1].pos == "VX")
            && tokens[i + 2].surface == ""
            && tokens[i + 2].pos == "EC"
        {
            // "가" + "지" 병합
            tokens[i].surface = "가지".to_string();
            tokens[i].end_pos = tokens[i + 1].end_pos;
            tokens.remove(i + 1);
            i += 2;
            continue;
        }
        i += 1;
    }

    // 196차: XPN 복합어 분리
    // "맨손/NNG" → "맨/XPN 손/NNG"
    // "맨발/NNG" → "맨/XPN 발/NNG"
    let xpn_compounds: std::collections::HashMap<&str, (&str, &str)> = [
        ("맨손", ("", "")),
        ("맨발", ("", "")),
        ("맨몸", ("", "")),
        ("맨땅", ("", "")),
    ]
    .into_iter()
    .collect();

    // 200차: "밤낮/NNG" → "밤/NNG 낮/NNG" 분리
    // "밤 낮" = "밤/NNG 낮/NNG"
    let mut i = 0;
    while i < tokens.len() {
        if tokens[i].pos == "NNG" && tokens[i].surface == "밤낮" {
            let start = tokens[i].start_pos;
            let end = tokens[i].end_pos;
            let first_len = "".chars().count();
            tokens[i] = SejongToken::new("", "NNG", start, start + first_len);
            tokens.insert(i + 1, SejongToken::new("", "NNG", start + first_len, end));
            i += 2;
            continue;
        }
        i += 1;
    }

    // 201차: XSN 접미사 분리 (주석 처리)
    // sample.tsv에서 대부분 "선생님/NNG"으로 단일 토큰 처리
    // "선생님 할머님" 한 케이스만 분리되어 있어 일관성 없음
    // 정확도 향상을 위해 분리하지 않음

    let mut i = 0;
    while i < tokens.len() {
        if tokens[i].pos == "NNG" {
            if let Some((prefix, noun)) = xpn_compounds.get(tokens[i].surface.as_str()) {
                let start = tokens[i].start_pos;
                let end = tokens[i].end_pos;
                let prefix_len = prefix.chars().count();
                tokens[i] = SejongToken::new(prefix, "XPN", start, start + prefix_len);
                tokens.insert(
                    i + 1,
                    SejongToken::new(noun, "NNG", start + prefix_len, end),
                );
                i += 2;
                continue;
            }
        }
        i += 1;
    }

    // 197차: "작은집" 복합 접두사 분리
    // "작은집/NNG" or "작/VA 은/ETM 집/NNG" → "작/XPN 은/XPN 집/NNG"
    // 단, MeCab이 "작/VA 은/ETM 집/NNG"로 분석하면 ETM→XPN 변환
    for i in 0..tokens.len().saturating_sub(1) {
        if tokens[i].surface == ""
            && tokens[i].pos == "VA"
            && tokens[i + 1].surface == ""
            && tokens[i + 1].pos == "ETM"
        {
            // 다음이 "집"인 경우 접두사로 변환
            if i + 2 < tokens.len() && tokens[i + 2].surface == "" {
                tokens[i].pos = "XPN".to_string();
                tokens[i + 1].pos = "XPN".to_string();
            }
        }
    }

    // 202차: 복합명사 병합
    // "무역/NNG + 수지/NNG" → "무역수지/NNG"
    // "여론/NNG + 조사/NNG" → "여론조사/NNG"
    // "시민/NNG + 단체/NNG" → "시민단체/NNG"
    // sample.tsv에서 단일 토큰으로 취급하는 복합명사들
    let compound_nouns: std::collections::HashSet<(&str, &str)> = [
        ("무역", "수지"),
        ("여론", "조사"),
        ("시민", "단체"),
        ("국민", "경제"),
        ("경제", "성장"),
        ("대통령", "선거"),
        ("정부", "정책"),
        ("환경", "보호"),
        ("인공", "지능"),
        ("형태소", "분석"),
    ]
    .into_iter()
    .collect();

    let mut i = 0;
    while i + 1 < tokens.len() {
        if tokens[i].pos == "NNG" && tokens[i + 1].pos == "NNG" {
            let pair = (tokens[i].surface.as_str(), tokens[i + 1].surface.as_str());
            if compound_nouns.contains(&pair) {
                let start = tokens[i].start_pos;
                let end = tokens[i + 1].end_pos;
                let merged = format!("{}{}", tokens[i].surface, tokens[i + 1].surface);
                tokens[i] = SejongToken::new(&merged, "NNG", start, end);
                tokens.remove(i + 1);
                continue;
            }
        }
        i += 1;
    }

    // 198차: "높이/NNG" → "높/VA 이/EC" 분리
    // "높이 낮이" = "높/VA 이/EC 낮/VA 이/EC"
    // 형용사 부사형 분리
    let va_ec_words: std::collections::HashMap<&str, &str> = [
        ("높이", ""),
        ("낮이", ""),
        ("깊이", ""),
        ("넓이", ""),
    ]
    .into_iter()
    .collect();

    // 199차: VA 어간 목록 (낮/NNG + 이/JKS 패턴용)
    let va_stems: std::collections::HashSet<&str> = ["", "", "", ""].into_iter().collect();

    let mut i = 0;
    while i < tokens.len() {
        // 패턴 1: "높이/NNG" 단일 토큰
        if tokens[i].pos == "NNG" {
            if let Some(&stem) = va_ec_words.get(tokens[i].surface.as_str()) {
                let start = tokens[i].start_pos;
                let end = tokens[i].end_pos;
                let stem_len = stem.chars().count();
                tokens[i] = SejongToken::new(stem, "VA", start, start + stem_len);
                tokens.insert(i + 1, SejongToken::new("", "EC", start + stem_len, end));
                i += 2;
                continue;
            }
        }
        // 패턴 2: "낮/NNG + 이/JKS" 두 토큰
        if i + 1 < tokens.len()
            && tokens[i].pos == "NNG"
            && va_stems.contains(tokens[i].surface.as_str())
            && tokens[i + 1].surface == ""
            && tokens[i + 1].pos == "JKS"
        {
            tokens[i].pos = "VA".to_string();
            tokens[i + 1].pos = "EC".to_string();
            i += 2;
            continue;
        }
        i += 1;
    }

    // 190차: "VV + 히다/NNP" → "VV + 히/VX + 다/EF"
    // "입히다" = "입/VV 히/VX 다/EF" (피사동 접미사)
    let mut i = 0;
    while i < tokens.len() {
        if i > 0
            && tokens[i].surface == "히다"
            && tokens[i].pos == "NNP"
            && tokens[i - 1].pos == "VV"
        {
            let start = tokens[i].start_pos;
            let end = tokens[i].end_pos;
            tokens[i] = SejongToken::new("", "VX", start, start + "".len());
            tokens.insert(i + 1, SejongToken::new("", "EF", start + "".len(), end));
            i += 2;
            continue;
        }
        i += 1;
    }
}