Skip to main content

nl3/
tagger.rs

1//! Part-of-speech tagging. Ports the tagging half of `lib/text/classify.js`.
2//!
3//! The original used the npm `pos` package (a Brill tagger). Analysis of the
4//! pipeline shows POS tags only ever change behavior in two ways: `reduce_parts`
5//! skips prepositions (`IN`) and WH-words (any tag starting with `W`), and
6//! numbers stay as values (`CD`). Entity *types* come from the grammar lexicon
7//! and predicates from vocabulary stemming — not from tags. So a small
8//! closed-class lexicon tagger reproduces every documented example.
9//!
10//! [`Tagger`] keeps this pluggable: a caller wanting general English POS
11//! tagging can implement the trait over, e.g., `rust-bert`'s `POSModel`.
12
13/// Tags text into `(token, Penn-Treebank-style tag)` pairs.
14pub trait Tagger {
15    /// Tag `text`, returning one `(token, tag)` pair per whitespace-separated
16    /// token in order.
17    fn tag(&self, text: &str) -> Vec<(String, String)>;
18}
19
20/// The default tagger: a closed-class lexicon. Recognizes prepositions (`IN`),
21/// WH-words (`WDT`), and cardinal numbers (`CD`); everything else is `NN` and
22/// falls through to the grammar/vocabulary logic.
23#[derive(Debug, Clone, Copy, Default)]
24pub struct LexiconTagger;
25
26impl Tagger for LexiconTagger {
27    fn tag(&self, text: &str) -> Vec<(String, String)> {
28        text.split_whitespace()
29            .map(|token| (token.to_string(), tag_word(token).to_string()))
30            .collect()
31    }
32}
33
34fn tag_word(word: &str) -> &'static str {
35    if !word.is_empty() && word.bytes().all(|b| b.is_ascii_digit()) {
36        return "CD";
37    }
38    match word.to_lowercase().as_str() {
39        "by" | "from" | "on" | "for" | "to" | "with" | "of" | "in" | "at" | "into" | "onto"
40        | "about" | "as" | "per" | "via" => "IN",
41        "who" | "which" | "that" | "what" | "whom" | "whose" | "where" | "when" | "why" | "how" => {
42            "WDT"
43        }
44        _ => "NN",
45    }
46}