const THAI_MIN_WORD: usize = 2;
pub(crate) const THAI_MIN_WORD_SPAN: usize = THAI_MIN_WORD * 2;
const THAI_ROOT_COMBINE_THRESHOLD: usize = 3;
const THAI_PREFIX_COMBINE_THRESHOLD: usize = 3;
const THAI_PAIYANNOI: char = '\u{0E2F}';
const THAI_MAIYAMOK: char = '\u{0E46}';
const POSSIBLE_WORD_LIST_MAX: usize = 20;
#[inline]
pub(crate) fn is_thai_dict_char(c: char) -> bool {
matches!(c as u32, 0x0E01..=0x0E3A | 0x0E40..=0x0E4E)
}
#[inline]
fn is_mark(c: char) -> bool {
matches!(c as u32, 0x0020 | 0x0E31 | 0x0E34..=0x0E3A | 0x0E47..=0x0E4E)
}
#[inline]
fn is_end_word(c: char) -> bool {
matches!(c as u32, 0x0E01..=0x0E30 | 0x0E32..=0x0E3A | 0x0E45..=0x0E4E)
}
#[inline]
fn is_begin_word(c: char) -> bool {
matches!(c as u32, 0x0E01..=0x0E2E | 0x0E40..=0x0E44)
}
#[inline]
fn is_suffix(c: char) -> bool {
c == THAI_PAIYANNOI || c == THAI_MAIYAMOK
}
#[cfg(feature = "segmentation-dict-lao")]
#[inline]
pub(crate) fn is_lao_dict_char(c: char) -> bool {
matches!(c as u32,
0x0E81..=0x0E82 | 0x0E84 | 0x0E86..=0x0E8A | 0x0E8C..=0x0EA3 | 0x0EA5
| 0x0EA7..=0x0EBD | 0x0EC0..=0x0EC4 | 0x0EC6 | 0x0EC8..=0x0ECE | 0x0EDC..=0x0EDF)
}
#[cfg(feature = "segmentation-dict-lao")]
#[inline]
fn is_mark_lao(c: char) -> bool {
matches!(c as u32, 0x0020 | 0x0EB1 | 0x0EB4..=0x0EBC | 0x0EC8..=0x0ECE)
}
#[cfg(feature = "segmentation-dict-lao")]
#[inline]
fn is_end_word_lao(c: char) -> bool {
matches!(c as u32,
0x0E81..=0x0E82 | 0x0E84 | 0x0E86..=0x0E8A | 0x0E8C..=0x0EA3 | 0x0EA5
| 0x0EA7..=0x0EBD | 0x0EC6 | 0x0EC8..=0x0ECE | 0x0EDC..=0x0EDF)
}
#[cfg(feature = "segmentation-dict-lao")]
#[inline]
fn is_begin_word_lao(c: char) -> bool {
matches!(c as u32, 0x0E81..=0x0EAE | 0x0EC0..=0x0EC4 | 0x0EDC..=0x0EDD)
}
#[cfg(feature = "segmentation-dict-km")]
#[inline]
pub(crate) fn is_khmer_dict_char(c: char) -> bool {
matches!(c as u32, 0x1780..=0x17D3 | 0x17D7 | 0x17DC..=0x17DD)
}
#[cfg(feature = "segmentation-dict-km")]
#[inline]
fn is_mark_khmer(c: char) -> bool {
matches!(c as u32, 0x0020 | 0x17B4..=0x17D3 | 0x17DD)
}
#[cfg(feature = "segmentation-dict-km")]
#[inline]
fn is_end_word_khmer(c: char) -> bool {
matches!(c as u32, 0x1780..=0x17D1 | 0x17D3 | 0x17D7 | 0x17DC..=0x17DD)
}
#[cfg(feature = "segmentation-dict-km")]
#[inline]
fn is_begin_word_khmer(c: char) -> bool {
matches!(c as u32, 0x1780..=0x17B3)
}
#[cfg(feature = "segmentation-dict-my")]
#[inline]
pub(crate) fn is_burmese_dict_char(c: char) -> bool {
matches!(c as u32, 0x1000..=0x103F | 0x1050..=0x108F | 0x109A..=0x109F)
}
#[cfg(feature = "segmentation-dict-my")]
#[inline]
fn is_mark_burmese(c: char) -> bool {
matches!(c as u32,
0x0020 | 0x102B..=0x103E | 0x1056..=0x1059 | 0x105E..=0x1060 | 0x1062..=0x1064
| 0x1067..=0x106D | 0x1071..=0x1074 | 0x1082..=0x108D | 0x108F | 0x109A..=0x109D)
}
#[cfg(feature = "segmentation-dict-my")]
#[inline]
fn is_end_word_burmese(c: char) -> bool {
is_burmese_dict_char(c)
}
#[cfg(feature = "segmentation-dict-my")]
#[inline]
fn is_begin_word_burmese(c: char) -> bool {
matches!(c as u32, 0x1000..=0x102A)
}
pub(crate) struct Lang {
dict: &'static [u8],
base: u32,
is_mark: fn(char) -> bool,
is_end_word: fn(char) -> bool,
is_begin_word: fn(char) -> bool,
thai_suffix: bool,
}
pub(crate) const THAI: Lang = Lang {
dict: THAI_DICT,
base: 0x0E00,
is_mark,
is_end_word,
is_begin_word,
thai_suffix: true,
};
#[cfg(feature = "segmentation-dict-lao")]
pub(crate) const LAO: Lang = Lang {
dict: LAO_DICT,
base: 0x0E00,
is_mark: is_mark_lao,
is_end_word: is_end_word_lao,
is_begin_word: is_begin_word_lao,
thai_suffix: false,
};
#[cfg(feature = "segmentation-dict-km")]
pub(crate) const KHMER: Lang = Lang {
dict: KHMER_DICT,
base: 0x1780,
is_mark: is_mark_khmer,
is_end_word: is_end_word_khmer,
is_begin_word: is_begin_word_khmer,
thai_suffix: false,
};
#[cfg(feature = "segmentation-dict-my")]
pub(crate) const BURMESE: Lang = Lang {
dict: BURMESE_DICT,
base: 0x1000,
is_mark: is_mark_burmese,
is_end_word: is_end_word_burmese,
is_begin_word: is_begin_word_burmese,
thai_suffix: false,
};
const THAI_DICT: &[u8] = include_bytes!("segment_dict.bin");
#[cfg(feature = "segmentation-dict-lao")]
const LAO_DICT: &[u8] = include_bytes!("segment_dict_lao.bin");
#[cfg(feature = "segmentation-dict-km")]
const KHMER_DICT: &[u8] = include_bytes!("segment_dict_km.bin");
#[cfg(feature = "segmentation-dict-my")]
const BURMESE_DICT: &[u8] = include_bytes!("segment_dict_my.bin");
#[inline]
fn rd_u32(blob: &[u8], o: usize) -> Option<u32> {
let b = blob.get(o..o.checked_add(4)?)?;
Some(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
}
#[inline]
fn rd_u16(blob: &[u8], o: usize) -> Option<u16> {
let b = blob.get(o..o.checked_add(2)?)?;
Some(u16::from_le_bytes([b[0], b[1]]))
}
struct Dict {
blob: &'static [u8],
n: usize,
root: usize,
bitmap_base: usize,
off_base: usize,
edges_base: usize,
base: u32,
wide: bool,
}
impl Dict {
fn load(blob: &'static [u8], base: u32) -> Option<Dict> {
let n = rd_u32(blob, 0)? as usize;
let e = rd_u32(blob, 4)? as usize;
let root = rd_u32(blob, 8)? as usize;
let wide = n > u16::MAX as usize || e > u16::MAX as usize;
let idx = if wide { 4 } else { 2 }; let rec = 1 + idx; let bitmap_base = 12;
let off_base = bitmap_base + n.div_ceil(8);
let edges_base = off_base.checked_add((n.checked_add(1)?).checked_mul(idx)?)?;
if edges_base.checked_add(e.checked_mul(rec)?)? > blob.len() || root >= n {
return None;
}
Some(Dict {
blob,
n,
root,
bitmap_base,
off_base,
edges_base,
base,
wide,
})
}
#[inline]
fn rd_idx(&self, o: usize) -> Option<usize> {
if self.wide {
rd_u32(self.blob, o).map(|v| v as usize)
} else {
rd_u16(self.blob, o).map(|v| v as usize)
}
}
#[inline]
fn is_final(&self, node: usize) -> bool {
if node >= self.n {
return false;
}
self.blob
.get(self.bitmap_base + node / 8)
.is_some_and(|byte| (byte >> (node % 8)) & 1 != 0)
}
#[inline]
fn edge_range(&self, node: usize) -> (usize, usize) {
let w = if self.wide { 4 } else { 2 };
let a = self.rd_idx(self.off_base + node * w).unwrap_or(0);
let b = self.rd_idx(self.off_base + (node + 1) * w).unwrap_or(0);
(a, b)
}
#[inline]
fn has_children(&self, node: usize) -> bool {
let (a, b) = self.edge_range(node);
a < b
}
#[inline]
fn child(&self, node: usize, sym: u8) -> Option<usize> {
let recw = if self.wide { 5 } else { 3 };
let (mut lo, mut hi) = self.edge_range(node);
while lo < hi {
let mid = lo + (hi - lo) / 2;
let rec = self.edges_base + mid * recw;
let s = *self.blob.get(rec)?;
match s.cmp(&sym) {
core::cmp::Ordering::Less => lo = mid + 1,
core::cmp::Ordering::Greater => hi = mid,
core::cmp::Ordering::Equal => return self.rd_idx(rec + 1),
}
}
None
}
}
#[inline]
fn sym(base: u32, c: char) -> Option<u8> {
(c as u32)
.checked_sub(base)
.filter(|d| *d <= 0xFF)
.map(|d| d as u8)
}
#[inline]
fn current32(run: &str, pos: usize) -> char {
run.get(pos..)
.and_then(|r| r.chars().next())
.unwrap_or('\0')
}
#[inline]
fn next32(run: &str, pos: &mut usize) -> char {
match run.get(*pos..).and_then(|r| r.chars().next()) {
Some(c) => {
*pos += c.len_utf8();
c
}
None => '\0',
}
}
#[inline]
fn previous32(run: &str, pos: &mut usize) -> char {
if *pos == 0 {
return '\0';
}
let mut i = *pos - 1;
while i > 0 && !run.is_char_boundary(i) {
i -= 1;
}
*pos = i;
current32(run, i)
}
struct PossibleWord {
count: usize,
prefix: usize, offset: usize, mark: usize, current: usize, cu: [usize; POSSIBLE_WORD_LIST_MAX], cp: [usize; POSSIBLE_WORD_LIST_MAX], }
impl PossibleWord {
fn new() -> Self {
PossibleWord {
count: 0,
prefix: 0,
offset: usize::MAX, mark: 0,
current: 0,
cu: [0; POSSIBLE_WORD_LIST_MAX],
cp: [0; POSSIBLE_WORD_LIST_MAX],
}
}
fn candidates(
&mut self,
dict: &Dict,
run: &str,
range_end: usize,
cursor: &mut usize,
) -> usize {
let start = *cursor;
if start != self.offset {
self.offset = start;
self.fill(dict, run, start, range_end);
if self.count == 0 {
*cursor = start;
}
}
if self.count > 0 {
*cursor = start + self.cu[self.count - 1];
}
self.current = self.count.saturating_sub(1);
self.mark = self.current;
self.count
}
fn fill(&mut self, dict: &Dict, run: &str, start: usize, range_end: usize) {
self.count = 0;
let mut node = dict.root;
let mut cp_matched = 0usize;
let mut consumed = 0usize;
let end = range_end.min(run.len());
let max_bytes = end - start;
for c in run[start..end].chars() {
let child = sym(dict.base, c).and_then(|s| dict.child(node, s));
consumed += c.len_utf8();
cp_matched += 1;
match child {
None => break, Some(nx) => {
node = nx;
if dict.is_final(node) {
if self.count < POSSIBLE_WORD_LIST_MAX {
self.cu[self.count] = consumed;
self.cp[self.count] = cp_matched;
self.count += 1;
}
if !dict.has_children(node) {
break; }
}
}
}
if consumed >= max_bytes {
break;
}
}
self.prefix = cp_matched;
}
fn accept_marked(&self, cursor: &mut usize) -> usize {
*cursor = self.offset + self.cu[self.mark];
self.cu[self.mark]
}
fn back_up(&mut self, cursor: &mut usize) -> bool {
if self.current > 0 {
self.current -= 1;
*cursor = self.offset + self.cu[self.current];
true
} else {
false
}
}
#[inline]
fn mark_current(&mut self) {
self.mark = self.current;
}
#[inline]
fn marked_cp_length(&self) -> usize {
self.cp[self.mark]
}
#[inline]
fn longest_prefix(&self) -> usize {
self.prefix
}
}
pub(crate) fn next_boundary(lang: &Lang, run: &str, start: usize) -> usize {
let range_end = run.len();
let Some(dict) = Dict::load(lang.dict, lang.base) else {
return range_end;
};
let mut w0 = PossibleWord::new();
let mut w1 = PossibleWord::new();
let mut w2 = PossibleWord::new();
let current = start;
let mut cursor = start;
let mut cu_word_length = 0usize;
let mut cp_word_length = 0usize;
let candidates = w0.candidates(&dict, run, range_end, &mut cursor);
if candidates == 1 {
cu_word_length = w0.accept_marked(&mut cursor);
cp_word_length = w0.marked_cp_length();
} else if candidates > 1 {
if cursor < range_end {
'search: loop {
if w1.candidates(&dict, run, range_end, &mut cursor) > 0 {
w0.mark_current();
if cursor >= range_end {
break 'search;
}
loop {
if w2.candidates(&dict, run, range_end, &mut cursor) > 0 {
w0.mark_current();
break 'search;
}
if !w1.back_up(&mut cursor) {
break;
}
}
}
if !w0.back_up(&mut cursor) {
break 'search;
}
}
}
cu_word_length = w0.accept_marked(&mut cursor);
cp_word_length = w0.marked_cp_length();
}
let mut uc: char;
if cursor < range_end && cp_word_length < THAI_ROOT_COMBINE_THRESHOLD {
if w0.candidates(&dict, run, range_end, &mut cursor) == 0
&& (cu_word_length == 0 || w0.longest_prefix() < THAI_PREFIX_COMBINE_THRESHOLD)
{
let mut remaining = (range_end - (current + cu_word_length)) as isize;
let mut chars = 0usize;
loop {
let pc_index = cursor;
let pc = next32(run, &mut cursor);
let pc_size = cursor - pc_index;
chars += pc_size;
remaining -= pc_size as isize;
if remaining <= 0 {
break;
}
uc = current32(run, cursor);
if (lang.is_end_word)(pc) && (lang.is_begin_word)(uc) {
let num = w1.candidates(&dict, run, range_end, &mut cursor);
cursor = current + cu_word_length + chars;
if num > 0 {
break;
}
}
}
cu_word_length += chars;
} else {
cursor = current + cu_word_length;
}
}
while cursor < range_end && (lang.is_mark)(current32(run, cursor)) {
let curr_pos = cursor;
next32(run, &mut cursor);
cu_word_length += cursor - curr_pos;
}
if lang.thai_suffix && cursor < range_end && cu_word_length > 0 {
let no_dict_follows = w0.candidates(&dict, run, range_end, &mut cursor) == 0;
uc = current32(run, cursor);
if no_dict_follows && is_suffix(uc) {
if uc == THAI_PAIYANNOI {
if !is_suffix(previous32(run, &mut cursor)) {
next32(run, &mut cursor); let paiyannoi_index = cursor;
next32(run, &mut cursor); cu_word_length += cursor - paiyannoi_index;
uc = current32(run, cursor);
} else {
next32(run, &mut cursor); }
}
if uc == THAI_MAIYAMOK {
if previous32(run, &mut cursor) != THAI_MAIYAMOK {
next32(run, &mut cursor);
let maiyamok_index = cursor;
next32(run, &mut cursor);
cu_word_length += cursor - maiyamok_index;
} else {
next32(run, &mut cursor);
}
}
}
}
(current + cu_word_length).min(range_end)
}