data(hanja): words, not only syllables

The import kept only readings of one syllable, so the Hanja search could
convert 한 but never 한자, 학교, or 대한민국 — the conversion every other
Korean input method offers.  libhangul's table has 187k readings; both
scripts now keep them all, and the search finds a word as readily as a
syllable.  The daemon pays for it: 24 MB instead of 12, and 170 ms to
start instead of 30.
This commit is contained in:
2026-08-17 12:50:05 +09:00
parent 3527bcd489
commit 2e53627b7d
7 changed files with 424873 additions and 45 deletions

View File

@@ -58,8 +58,11 @@ typed keys, and what they spell in the current language, against a prefix
of every emoji's CLDR name and keywords in English, Korean, and Japanese, of every emoji's CLDR name and keywords in English, Korean, and Japanese,
and against ASCII symbol aliases such as `->` and `<=`. `Ctrl+H` takes the and against ASCII symbol aliases such as `->` and `<=`. `Ctrl+H` takes the
syllable being composed as its query and composes on from it; `Esc` gives syllable being composed as its query and composes on from it; `Esc` gives
the syllable back; the Hanja dictionary lists one modern Hangul syllable at the syllable back. The Hanja dictionary converts a word as well as a
a time. Leaving the field or clicking elsewhere commits what is pending. syllable, so `Ctrl+H` and then 한자 gives 漢字, and 대한민국 gives 大韓民國;
a syllable already committed is the application's text, not the engine's,
and cannot be converted. Leaving the field or
clicking elsewhere commits what is pending.
## Preedit and candidates ## Preedit and candidates

View File

@@ -3,18 +3,19 @@
## Korean Hanja data ## Korean Hanja data
`hanja.src` is the tracked, reviewable source for Korean Hanja conversion. `hanja.src` is the tracked, reviewable source for Korean Hanja conversion.
Every data row is exactly one Hanja character, a tab, and one modern Hangul Every data row is Hanja, a tab, and its modern Hangul reading, a character
syllable: or a word:
``` ```
漢 한 漢 한
漢字 한자
``` ```
It contains no word rows such as `견출지` or `방학`. `mkhanja` validates this `mkhanja` validates that contract and groups rows by their reading to
contract and groups rows by their Hangul reading to produce the existing produce the runtime dictionary. Candidate order follows source order. The
runtime dictionary format. Candidate order follows source order. The source source keeps all retained pairs for review; the generated dictionary stores
keeps all retained pairs for review; the generated dictionary stores the first the first 128 candidates per reading because that is the engine's lookup
128 candidates per reading because that is the engine's lookup limit. limit.
The source is derived from libhangul release tag `libhangul-0.2.0`. The The source is derived from libhangul release tag `libhangul-0.2.0`. The
annotated tag object is `20afc38922e3595ee3ed5b186f2ea05afe663763`, its annotated tag object is `20afc38922e3595ee3ed5b186f2ea05afe663763`, its
@@ -30,11 +31,10 @@ map/libhangul2hanja upstream >map/hanja.src
map/mkhanja >map/hanja.dict map/mkhanja >map/hanja.dict
``` ```
`libhangul2hanja` retains only rows whose reading is one modern Hangul `libhangul2hanja` retains only rows whose reading is modern Hangul syllables
syllable and whose value is one Hanja character supported by the current throughout and whose value is Hanja the popup can draw: U+3400U+4DBF,
popup: U+3400U+4DBF, U+4E00U+9FFF, or U+F900U+FAFF. Thus word readings, U+4E00U+9FFF, or U+F900U+FAFF. Thus jamo readings, mixed values, and
multi-character values, jamo readings, and supplementary-plane ideographs supplementary-plane ideographs are omitted. The import preserves the upstream license header and row order.
are omitted. The import preserves the upstream license header and row order.
The retained data is BSD 3-Clause licensed by Choe Hwanjin; the complete text The retained data is BSD 3-Clause licensed by Choe Hwanjin; the complete text
is in `LICENSES/BSD-3-Clause-libhangul-hanja.txt`. is in `LICENSES/BSD-3-Clause-libhangul-hanja.txt`.

File diff suppressed because it is too large Load Diff

238096
map/hanja.src

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,18 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Extract single-character Hanja readings from libhangul data.""" """Extract Hanja readings from libhangul data."""
import sys import sys
from pathlib import Path from pathlib import Path
def ishangul(s): def ishangul(s):
return len(s) == 1 and 0xAC00 <= ord(s) <= 0xD7A3 return s != "" and all(0xAC00 <= ord(c) <= 0xD7A3 for c in s)
def ishanja(s): def ishanja(s):
if len(s) != 1: return s != "" and all(0x3400 <= ord(c) <= 0x4DBF
return False or 0x4E00 <= ord(c) <= 0x9FFF
c = ord(s) or 0xF900 <= ord(c) <= 0xFAFF for c in s)
return (0x3400 <= c <= 0x4DBF
or 0x4E00 <= c <= 0x9FFF
or 0xF900 <= c <= 0xFAFF)
def extract(src, name): def extract(src, name):
@@ -44,7 +41,7 @@ def extract(src, name):
seen.add(pair) seen.add(pair)
entries.append(pair) entries.append(pair)
if not entries: if not entries:
raise ValueError(f"{name}: no single-character Hanja readings") raise ValueError(f"{name}: no Hanja readings")
return comments, entries return comments, entries

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Write hanja.dict from a one-Hanja-per-row UTF-8 source.""" """Write hanja.dict from a Hanja-per-row UTF-8 source."""
import sys import sys
from pathlib import Path from pathlib import Path
@@ -10,16 +10,13 @@ MAXCANDIDATES = 128
def ishangul(s): def ishangul(s):
return len(s) == 1 and 0xAC00 <= ord(s) <= 0xD7A3 return s != "" and all(0xAC00 <= ord(c) <= 0xD7A3 for c in s)
def ishanja(s): def ishanja(s):
if len(s) != 1: return s != "" and all(0x3400 <= ord(c) <= 0x4DBF
return False or 0x4E00 <= ord(c) <= 0x9FFF
c = ord(s) or 0xF900 <= ord(c) <= 0xFAFF for c in s)
return (0x3400 <= c <= 0x4DBF
or 0x4E00 <= c <= 0x9FFF
or 0xF900 <= c <= 0xFAFF)
def read(path): def read(path):
@@ -42,9 +39,9 @@ def read(path):
raise ValueError(f"{path}:{lineno}: need Hanja<TAB>reading") raise ValueError(f"{path}:{lineno}: need Hanja<TAB>reading")
hanja, reading = fields hanja, reading = fields
if not ishanja(hanja): if not ishanja(hanja):
raise ValueError(f"{path}:{lineno}: need one BMP Hanja character") raise ValueError(f"{path}:{lineno}: need BMP Hanja")
if not ishangul(reading): if not ishangul(reading):
raise ValueError(f"{path}:{lineno}: need one Hangul syllable") raise ValueError(f"{path}:{lineno}: need Hangul syllables")
pair = (hanja, reading) pair = (hanja, reading)
if pair in seen: if pair in seen:
raise ValueError(f"{path}:{lineno}: duplicate Hanja reading") raise ValueError(f"{path}:{lineno}: duplicate Hanja reading")

View File

@@ -42,25 +42,27 @@ class MkhanjaTest(unittest.TestCase):
self.addCleanup(tmp.cleanup) self.addCleanup(tmp.cleanup)
return path return path
def test_imports_single_bmp_hanja_rows(self): def test_imports_bmp_hanja_rows(self):
result = run( result = run(
IMPORT, IMPORT,
self.source( self.source(
"# Copyright holder\n" "# Copyright holder\n"
"# BSD license\n" "# BSD license\n"
"한:漢:first\n" "\ud55c:\u6f22:first\n"
"가:㐀:extension A\n" "\uac00:\u3400:extension A\n"
"김:金:compatibility\n" "\uae40:\u91d1:compatibility\n"
"방학:放:word reading\n" "\ud55c\uc790:\u6f22\u5b57:word\n"
"학:學校:word value\n" "\ud55c:\U00020000:astral\n"
"한:𠀀:astral\n" "\u3131:\u52a0:jamo\n"
"ㄱ:加:jamo\n" "\ud55c\uae00:\u97d3glyph:mixed\n"
), ),
) )
self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.returncode, 0, result.stderr)
lines = result.stdout.splitlines() lines = result.stdout.splitlines()
self.assertEqual(lines[:2], ["# Copyright holder", "# BSD license"]) self.assertEqual(lines[:2], ["# Copyright holder", "# BSD license"])
self.assertEqual(lines[2:], ["", "\t", "\t", "\t"]) self.assertEqual(lines[2:], ["", "\u6f22\t\ud55c", "\u3400\t\uac00",
"\u91d1\t\uae40",
"\u6f22\u5b57\t\ud55c\uc790"])
def test_import_rejects_malformed_and_duplicate_rows(self): def test_import_rejects_malformed_and_duplicate_rows(self):
bad = [ bad = [
@@ -91,6 +93,7 @@ class MkhanjaTest(unittest.TestCase):
"\t\n" "\t\n"
"\t\n" "\t\n"
"\t\n" "\t\n"
"漢字\t한자\n"
), ),
) )
self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.returncode, 0, result.stderr)
@@ -103,13 +106,14 @@ class MkhanjaTest(unittest.TestCase):
"\t漢 韓", "\t漢 韓",
"\t㐀 家", "\t㐀 家",
"\t", "\t",
"한자\t漢字",
], ],
) )
def test_generator_rejects_bad_rows(self): def test_generator_rejects_bad_rows(self):
bad = [ bad = [
"\t\n", "a\t\n",
"\t\n", "\ta\n",
"𠀀\t\n", "𠀀\t\n",
"\t\n\t\n", "\t\n\t\n",
"漢 한\n", "漢 한\n",