fn all_digits(word: &str) -> bool {
!word.is_empty() && word.chars().all(|c| c.is_ascii_digit() || matches!(c, '۰'..='۹'))
}
fn is_punc(word: &str) -> bool {
const PUNC: &[&str] = &[".", ",", "!", "?", "؟", "،", ":", ";", "(", ")", "[", "]", "«", "»", "-"];
PUNC.contains(&word)
}
pub fn ner_features(sentence: &[&str], index: usize) -> Vec<String> {
let word = sentence[index];
let n = sentence.len();
let prev = if index > 0 { sentence[index - 1] } else { "" };
let prev2 = if index > 1 { sentence[index - 2] } else { "" };
let next = if index + 1 < n { sentence[index + 1] } else { "" };
let next2 = if index + 2 < n { sentence[index + 2] } else { "" };
let chars: Vec<char> = word.chars().collect();
let len = chars.len();
let prefix = |k: usize| -> String { chars.iter().take(k).collect() };
let suffix = |k: usize| -> String {
if len >= k {
chars[len - k..].iter().collect()
} else {
chars.iter().collect()
}
};
let mut f = vec![
format!("word={}", word),
format!("pfx1={}", prefix(1)),
format!("pfx2={}", prefix(2)),
format!("pfx3={}", prefix(3)),
format!("pfx4={}", prefix(4)),
format!("sfx1={}", suffix(1)),
format!("sfx2={}", suffix(2)),
format!("sfx3={}", suffix(3)),
format!("sfx4={}", suffix(4)),
format!("prev={}", prev),
format!("prev2={}", prev2),
format!("next={}", next),
format!("next2={}", next2),
format!("prev+word={}+{}", prev, word),
format!("word+next={}+{}", word, next),
format!("is_digit={}", all_digits(word)),
format!("is_punc={}", is_punc(word)),
format!("word_len={}", len.min(10)),
];
if index == 0 { f.push("BOS".to_string()); }
if index == n - 1 { f.push("EOS".to_string()); }
f
}
pub fn sentence_ner_features(sentence: &[&str]) -> Vec<Vec<String>> {
(0..sentence.len()).map(|i| ner_features(sentence, i)).collect()
}