fontisan 0.4.9 → 0.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,429 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ class Stitcher
5
+ module PartitionStrategy
6
+ # Partition codepoints by Unicode Blocks.txt block.
7
+ #
8
+ # Each non-empty Unicode block becomes one partition. If a single
9
+ # block alone exceeds +cap+, raises PartitionCapExceededError —
10
+ # every block is treated as atomic because there is no finer
11
+ # Unicode-defined boundary inside a block. (Callers who need to
12
+ # split a block further must use ByPlane with explicit carve-outs
13
+ # or implement a custom partitioner.)
14
+ #
15
+ # Partition names follow the canonical Unicode block name with
16
+ # spaces replaced by underscores and a +block_+ prefix:
17
+ #
18
+ # "Basic Latin" => :block_basic_latin
19
+ # "CJK Unified Ideographs" => :block_cjk_unified_ideographs
20
+ #
21
+ # Codepoints not covered by any block in {BLOCKS} (unassigned or
22
+ # in a block omitted from this list) fall into +:block_other+.
23
+ class ByBlock < Base
24
+ # Unicode 16.0 block ranges. Source: Unicode Blocks.txt.
25
+ # Covers all assigned blocks in BMP, SMP, SIP, TIP, and SSP.
26
+ # Unassigned planes (4..13) are omitted — codepoints there
27
+ # fall into +:block_other+, which is the right behavior for
28
+ # partitioning fonts that target current Unicode.
29
+ #
30
+ # The data is inlined (rather than loaded from an external
31
+ # file) so the partitioner is self-contained: no YAML load
32
+ # at startup, no data file to ship. If the list ever needs to
33
+ # be data-driven, swap BLOCKS for a CSV/YAML loader behind
34
+ # the same constant — callers don't care about the source
35
+ # (OCP).
36
+ # rubocop:disable Metrics/CollectionLiteralLength
37
+ BLOCKS = {
38
+ # BMP (Plane 0)
39
+ "Basic_Latin" => 0x0000..0x007F,
40
+ "Latin-1_Supplement" => 0x0080..0x00FF,
41
+ "Latin_Extended-A" => 0x0100..0x017F,
42
+ "Latin_Extended-B" => 0x0180..0x024F,
43
+ "IPA_Extensions" => 0x0250..0x02AF,
44
+ "Spacing_Modifier_Letters" => 0x02B0..0x02FF,
45
+ "Combining_Diacritical_Marks" => 0x0300..0x036F,
46
+ "Greek_and_Coptic" => 0x0370..0x03FF,
47
+ "Cyrillic" => 0x0400..0x04FF,
48
+ "Cyrillic_Supplement" => 0x0500..0x052F,
49
+ "Armenian" => 0x0530..0x058F,
50
+ "Hebrew" => 0x0590..0x05FF,
51
+ "Arabic" => 0x0600..0x06FF,
52
+ "Syriac" => 0x0700..0x074F,
53
+ "Arabic_Supplement" => 0x0750..0x077F,
54
+ "Arabic_Extended-A" => 0x08A0..0x08FF,
55
+ "Arabic_Extended-B" => 0x0870..0x089F,
56
+ "Thaana" => 0x0780..0x07BF,
57
+ "NKo" => 0x07C0..0x07FF,
58
+ "Samaritan" => 0x0800..0x083F,
59
+ "Mandaic" => 0x0840..0x085F,
60
+ "Syriac_Supplement" => 0x0860..0x086F,
61
+ "Devanagari" => 0x0900..0x097F,
62
+ "Bengali" => 0x0980..0x09FF,
63
+ "Gurmukhi" => 0x0A00..0x0A7F,
64
+ "Gujarati" => 0x0A80..0x0AFF,
65
+ "Oriya" => 0x0B00..0x0B7F,
66
+ "Tamil" => 0x0B80..0x0BFF,
67
+ "Telugu" => 0x0C00..0x0C7F,
68
+ "Kannada" => 0x0C80..0x0CFF,
69
+ "Malayalam" => 0x0D00..0x0D7F,
70
+ "Sinhala" => 0x0D80..0x0DFF,
71
+ "Thai" => 0x0E00..0x0E7F,
72
+ "Lao" => 0x0E80..0x0EFF,
73
+ "Tibetan" => 0x0F00..0x0FFF,
74
+ "Myanmar" => 0x1000..0x109F,
75
+ "Georgian" => 0x10A0..0x10FF,
76
+ "Hangul_Jamo" => 0x1100..0x11FF,
77
+ "Ethiopic" => 0x1200..0x137F,
78
+ "Ethiopic_Supplement" => 0x1380..0x139F,
79
+ "Cherokee" => 0x13A0..0x13FF,
80
+ "Unified_Canadian_Aboriginal_Syllabics" => 0x1400..0x167F,
81
+ "Ogham" => 0x1680..0x169F,
82
+ "Runic" => 0x16A0..0x16FF,
83
+ "Tagalog" => 0x1700..0x171F,
84
+ "Hanunoo" => 0x1720..0x173F,
85
+ "Buhid" => 0x1740..0x175F,
86
+ "Tagbanwa" => 0x1760..0x177F,
87
+ "Khmer" => 0x1780..0x17FF,
88
+ "Mongolian" => 0x1800..0x18AF,
89
+ "Unified_Canadian_Aboriginal_Syllabics_Extended" => 0x18B0..0x18FF,
90
+ "Limbu" => 0x1900..0x194F,
91
+ "Tai_Le" => 0x1950..0x197F,
92
+ "New_Tai_Lue" => 0x1980..0x19DF,
93
+ "Khmer_Symbols" => 0x19E0..0x19FF,
94
+ "Buginese" => 0x1A00..0x1A1F,
95
+ "Tai_Tham" => 0x1A20..0x1AAF,
96
+ "Combining_Diacritical_Marks_Extended" => 0x1AB0..0x1AFF,
97
+ "Balinese" => 0x1B00..0x1B7F,
98
+ "Sundanese" => 0x1B80..0x1BBF,
99
+ "Batak" => 0x1BC0..0x1BFF,
100
+ "Lepcha" => 0x1C00..0x1C4F,
101
+ "Ol_Chiki" => 0x1C50..0x1C7F,
102
+ "Cyrillic_Extended-C" => 0x1C80..0x1C8F,
103
+ "Georgian_Extended" => 0x1C90..0x1CBF,
104
+ "Sundanese_Supplement" => 0x1CC0..0x1CCF,
105
+ "Vedic_Extensions" => 0x1CD0..0x1CFF,
106
+ "Phonetic_Extensions" => 0x1D00..0x1D7F,
107
+ "Phonetic_Extensions_Supplement" => 0x1D80..0x1DBF,
108
+ "Combining_Diacritical_Marks_Supplement" => 0x1DC0..0x1DFF,
109
+ "Latin_Extended_Additional" => 0x1E00..0x1EFF,
110
+ "Greek_Extended" => 0x1F00..0x1FFF,
111
+ "General_Punctuation" => 0x2000..0x206F,
112
+ "Superscripts_and_Subscripts" => 0x2070..0x209F,
113
+ "Currency_Symbols" => 0x20A0..0x20CF,
114
+ "Combining_Diacritical_Marks_for_Symbols" => 0x20D0..0x20FF,
115
+ "Letterlike_Symbols" => 0x2100..0x214F,
116
+ "Number_Forms" => 0x2150..0x218F,
117
+ "Arrows" => 0x2190..0x21FF,
118
+ "Mathematical_Operators" => 0x2200..0x22FF,
119
+ "Miscellaneous_Technical" => 0x2300..0x23FF,
120
+ "Control_Pictures" => 0x2400..0x243F,
121
+ "Optical_Character_Recognition" => 0x2440..0x245F,
122
+ "Enclosed_Alphanumerics" => 0x2460..0x24FF,
123
+ "Box_Drawing" => 0x2500..0x257F,
124
+ "Block_Elements" => 0x2580..0x259F,
125
+ "Geometric_Shapes" => 0x25A0..0x25FF,
126
+ "Miscellaneous_Symbols" => 0x2600..0x26FF,
127
+ "Dingbats" => 0x2700..0x27BF,
128
+ "Miscellaneous_Mathematical_Symbols-A" => 0x27C0..0x27EF,
129
+ "Supplemental_Arrows-A" => 0x27F0..0x27FF,
130
+ "Braille_Patterns" => 0x2800..0x28FF,
131
+ "Supplemental_Arrows-B" => 0x2900..0x297F,
132
+ "Miscellaneous_Mathematical_Symbols-B" => 0x2980..0x29FF,
133
+ "Supplemental_Mathematical_Operators" => 0x2A00..0x2AFF,
134
+ "Miscellaneous_Symbols_and_Arrows" => 0x2B00..0x2BFF,
135
+ "Glagolitic" => 0x2C00..0x2C5F,
136
+ "Latin_Extended-C" => 0x2C60..0x2C7F,
137
+ "Coptic" => 0x2C80..0x2CFF,
138
+ "Georgian_Supplement" => 0x2D00..0x2D2F,
139
+ "Tifinagh" => 0x2D30..0x2D7F,
140
+ "Ethiopic_Extended" => 0x2D80..0x2DDF,
141
+ "Supplemental_Punctuation" => 0x2E00..0x2E7F,
142
+ "CJK_Radicals_Supplement" => 0x2E80..0x2EFF,
143
+ "Kangxi_Radicals" => 0x2F00..0x2FDF,
144
+ "Ideographic_Description_Characters" => 0x2FF0..0x2FFF,
145
+ "CJK_Symbols_and_Punctuation" => 0x3000..0x303F,
146
+ "Hiragana" => 0x3040..0x309F,
147
+ "Katakana" => 0x30A0..0x30FF,
148
+ "Bopomofo" => 0x3100..0x312F,
149
+ "Hangul_Compatibility_Jamo" => 0x3130..0x318F,
150
+ "Kanbun" => 0x3190..0x319F,
151
+ "Bopomofo_Extended" => 0x31A0..0x31BF,
152
+ "CJK_Strokes" => 0x31C0..0x31EF,
153
+ "Katakana_Phonetic_Extensions" => 0x31F0..0x31FF,
154
+ "Enclosed_CJK_Letters_and_Months" => 0x3200..0x32FF,
155
+ "CJK_Compatibility" => 0x3300..0x33FF,
156
+ "CJK_Unified_Ideographs_Extension_A" => 0x3400..0x4DBF,
157
+ "Yijing_Hexagram_Symbols" => 0x4DC0..0x4DFF,
158
+ "CJK_Unified_Ideographs" => 0x4E00..0x9FFF,
159
+ "Yi_Syllables" => 0xA000..0xA48F,
160
+ "Yi_Radicals" => 0xA490..0xA4CF,
161
+ "Lisu" => 0xA4D0..0xA4FF,
162
+ "Vai" => 0xA500..0xA63F,
163
+ "Cyrillic_Extended-B" => 0xA640..0xA69F,
164
+ "Bamum" => 0xA6A0..0xA6FF,
165
+ "Modifier_Tone_Letters" => 0xA700..0xA71F,
166
+ "Latin_Extended-D" => 0xA720..0xA7FF,
167
+ "Syloti_Nagri" => 0xA800..0xA82F,
168
+ "Common_Indic_Number_Forms" => 0xA830..0xA83F,
169
+ "Phags-pa" => 0xA840..0xA87F,
170
+ "Saurashtra" => 0xA880..0xA8DF,
171
+ "Devanagari_Extended" => 0xA8E0..0xA8FF,
172
+ "Kayah_Li" => 0xA900..0xA92F,
173
+ "Rejang" => 0xA930..0xA95F,
174
+ "Hangul_Jamo_Extended-A" => 0xA960..0xA97F,
175
+ "Javanese" => 0xA980..0xA9DF,
176
+ "Myanmar_Extended-B" => 0xA9E0..0xA9FF,
177
+ "Cham" => 0xAA00..0xAA5F,
178
+ "Myanmar_Extended-A" => 0xAA60..0xAA7F,
179
+ "Tai_Viet" => 0xAA80..0xAADF,
180
+ "Meetei_Mayek_Extensions" => 0xAAE0..0xAAFF,
181
+ "Ethiopic_Extended-A" => 0xAB00..0xAB2F,
182
+ "Latin_Extended-E" => 0xAB30..0xAB6F,
183
+ "Cherokee_Supplement" => 0xAB70..0xABBF,
184
+ "Meetei_Mayek" => 0xABC0..0xABFF,
185
+ "Hangul_Syllables" => 0xAC00..0xD7AF,
186
+ "Hangul_Jamo_Extended-B" => 0xD7B0..0xD7FF,
187
+ "High_Surrogates" => 0xD800..0xDB7F,
188
+ "High_Private_Use_Surrogates" => 0xDB80..0xDBFF,
189
+ "Low_Surrogates" => 0xDC00..0xDFFF,
190
+ "Private_Use_Area" => 0xE000..0xF8FF,
191
+ "CJK_Compatibility_Ideographs" => 0xF900..0xFAFF,
192
+ "Alphabetic_Presentation_Forms" => 0xFB00..0xFB4F,
193
+ "Arabic_Presentation_Forms-A" => 0xFB50..0xFDFF,
194
+ "Variation_Selectors" => 0xFE00..0xFE0F,
195
+ "Vertical_Forms" => 0xFE10..0xFE1F,
196
+ "Combining_Half_Marks" => 0xFE20..0xFE2F,
197
+ "CJK_Compatibility_Forms" => 0xFE30..0xFE4F,
198
+ "Small_Form_Variants" => 0xFE50..0xFE6F,
199
+ "Arabic_Presentation_Forms-B" => 0xFE70..0xFEFF,
200
+ "Halfwidth_and_Fullwidth_Forms" => 0xFF00..0xFFEF,
201
+ "Specials" => 0xFFF0..0xFFFF,
202
+
203
+ # SMP (Plane 1)
204
+ "Linear_B_Syllabary" => 0x10000..0x1003F,
205
+ "Linear_B_Ideograms" => 0x10080..0x100FF,
206
+ "Aegean_Numbers" => 0x10100..0x1013F,
207
+ "Ancient_Greek_Numbers" => 0x10140..0x1018F,
208
+ "Ancient_Symbols" => 0x10190..0x101CF,
209
+ "Phaistos_Disc" => 0x101D0..0x101FF,
210
+ "Lycian" => 0x10280..0x1029F,
211
+ "Carian" => 0x102A0..0x102DF,
212
+ "Coptic_Epact_Numbers" => 0x102E0..0x102FF,
213
+ "Old_Italic" => 0x10300..0x1032F,
214
+ "Gothic" => 0x10330..0x1034F,
215
+ "Old_Permic" => 0x10350..0x1037F,
216
+ "Ugaritic" => 0x10380..0x1039F,
217
+ "Old_Persian" => 0x103A0..0x103DF,
218
+ "Deseret" => 0x10400..0x1044F,
219
+ "Shavian" => 0x10450..0x1047F,
220
+ "Osmanya" => 0x10480..0x104AF,
221
+ "Osage" => 0x104B0..0x104FF,
222
+ "Elbasan" => 0x10500..0x1052F,
223
+ "Caucasian_Albanian" => 0x10530..0x1056F,
224
+ "Vithkuqi" => 0x10570..0x105BF,
225
+ "Linear_A" => 0x10600..0x1077F,
226
+ "Latin_Extended-F" => 0x10780..0x107BF,
227
+ "Cypriot_Syllabary" => 0x10800..0x1083F,
228
+ "Imperial_Aramaic" => 0x10840..0x1085F,
229
+ "Palmyrene" => 0x10860..0x1087F,
230
+ "Nabataean" => 0x10880..0x108AF,
231
+ "Hatran" => 0x108E0..0x108FF,
232
+ "Phoenician" => 0x10900..0x1091F,
233
+ "Lydian" => 0x10920..0x1093F,
234
+ "Meroitic_Hieroglyphs" => 0x10980..0x1099F,
235
+ "Meroitic_Cursive" => 0x109A0..0x109FF,
236
+ "Kharoshthi" => 0x10A00..0x10A5F,
237
+ "Old_South_Arabian" => 0x10A60..0x10A7F,
238
+ "Old_North_Arabian" => 0x10A80..0x10A9F,
239
+ "Manichaean" => 0x10AC0..0x10AFF,
240
+ "Avestan" => 0x10B00..0x10B3F,
241
+ "Inscriptional_Parthian" => 0x10B40..0x10B5F,
242
+ "Inscriptional_Pahlavi" => 0x10B60..0x10B7F,
243
+ "Psalter_Pahlavi" => 0x10B80..0x10BAF,
244
+ "Old_Turkic" => 0x10C00..0x10C4F,
245
+ "Old_Hungarian" => 0x10C80..0x10CFF,
246
+ "Hanifi_Rohingya" => 0x10D00..0x10D3F,
247
+ "Garay" => 0x10D40..0x10D8F,
248
+ "Rumi_Numeral_Symbols" => 0x10E60..0x10E7F,
249
+ "Yezidi" => 0x10E80..0x10EBF,
250
+ "Arabic_Extended-C" => 0x10EC0..0x10EFF,
251
+ "Old_Sogdian" => 0x10F00..0x10F2F,
252
+ "Sogdian" => 0x10F30..0x10F6F,
253
+ "Old_Uyghur" => 0x10F70..0x10FAF,
254
+ "Chorasmian" => 0x10FB0..0x10FBF,
255
+ "Elymaic" => 0x10FE0..0x10FEF,
256
+ "Brahmi" => 0x11000..0x1107F,
257
+ "Kaithi" => 0x11080..0x110CF,
258
+ "Sora_Sompeng" => 0x110D0..0x110FF,
259
+ "Chakma" => 0x11100..0x1114F,
260
+ "Mahajani" => 0x11150..0x1117F,
261
+ "Sharada" => 0x11180..0x111DF,
262
+ "Sinhala_Archaic_Numbers" => 0x111E0..0x111FF,
263
+ "Khojki" => 0x11200..0x1124F,
264
+ "Multani" => 0x11280..0x112AF,
265
+ "Khudawadi" => 0x112B0..0x112FF,
266
+ "Grantha" => 0x11300..0x1137F,
267
+ "Tulu-Tigalari" => 0x11380..0x113FF,
268
+ "Newa" => 0x11400..0x1147F,
269
+ "Tirhuta" => 0x11480..0x114DF,
270
+ "Siddham" => 0x11580..0x115FF,
271
+ "Modi" => 0x11600..0x1165F,
272
+ "Mongolian_Supplement" => 0x11660..0x1167F,
273
+ "Takri" => 0x11680..0x116CF,
274
+ "Myanmar_Extended-C" => 0x116D0..0x116FF,
275
+ "Ahom" => 0x11700..0x1174F,
276
+ "Dogra" => 0x11800..0x1184F,
277
+ "Warang_Citi" => 0x118A0..0x118FF,
278
+ "Dives_Akuru" => 0x11900..0x1195F,
279
+ "Nandinagari" => 0x119A0..0x119FF,
280
+ "Zanabazar_Square" => 0x11A00..0x11A4F,
281
+ "Soyombo" => 0x11A50..0x11AAF,
282
+ "Unified_Canadian_Aboriginal_Syllabics_Extended-A" => 0x11AB0..0x11ABF,
283
+ "Pau_Cin_Hmo" => 0x11AC0..0x11AFF,
284
+ "Bhaiksuki" => 0x11C00..0x11C6F,
285
+ "Marchen" => 0x11C70..0x11CBF,
286
+ "Masaram_Gondi" => 0x11D00..0x11D5F,
287
+ "Gunjala_Gondi" => 0x11D60..0x11DAF,
288
+ "Makasar" => 0x11EE0..0x11EFF,
289
+ "Kawi" => 0x11F00..0x11F5F,
290
+ "Lisu_Supplement" => 0x11FB0..0x11FBF,
291
+ "Tamil_Supplement" => 0x11FC0..0x11FFF,
292
+ "Cuneiform" => 0x12000..0x123FF,
293
+ "Cuneiform_Numbers_and_Punctuation" => 0x12400..0x1247F,
294
+ "Early_Dynastic_Cuneiform" => 0x12480..0x1254F,
295
+ "Cypro-Minoan" => 0x12F90..0x12FFF,
296
+ "Egyptian_Hieroglyphs" => 0x13000..0x1342F,
297
+ "Egyptian_Hieroglyph_Format_Controls" => 0x13430..0x1345F,
298
+ "Egyptian_Hieroglyphs_Extended-A" => 0x13460..0x143FF,
299
+ "Anatolian_Hieroglyphs" => 0x14400..0x1467F,
300
+ "Bamum_Supplement" => 0x16800..0x16A3F,
301
+ "Mro" => 0x16A40..0x16A6F,
302
+ "Tangsa" => 0x16A70..0x16ACF,
303
+ "Bassa_Vah" => 0x16AD0..0x16AFF,
304
+ "Pahawh_Hmong" => 0x16B00..0x16B8F,
305
+ "Medefaidrin" => 0x16E40..0x16E9F,
306
+ "Miao" => 0x16F00..0x16F9F,
307
+ "Ideographic_Symbols_and_Punctuation" => 0x16FE0..0x16FFF,
308
+ "Tangut" => 0x17000..0x187FF,
309
+ "Tangut_Components" => 0x18800..0x18AFF,
310
+ "Khitan_Small_Script" => 0x18B00..0x18CFF,
311
+ "Tangut_Supplement" => 0x18D00..0x18D7F,
312
+ "Kana_Supplement" => 0x1B000..0x1B0FF,
313
+ "Kana_Extended-A" => 0x1B100..0x1B12F,
314
+ "Small_Kana_Extension" => 0x1B130..0x1B16F,
315
+ "Nushu" => 0x1B170..0x1B2FF,
316
+ "Duployan" => 0x1BC00..0x1BC9F,
317
+ "Shorthand_Format_Controls" => 0x1BCA0..0x1BCAF,
318
+ "Znamenny_Musical_Notation" => 0x1CF00..0x1CFCF,
319
+ "Byzantine_Musical_Symbols" => 0x1D000..0x1D0FF,
320
+ "Musical_Symbols" => 0x1D100..0x1D1FF,
321
+ "Ancient_Greek_Musical_Notation" => 0x1D200..0x1D24F,
322
+ "Kaktovik_Numerals" => 0x1D2C0..0x1D2FF,
323
+ "Tai_Xuan_Jing_Symbols" => 0x1D300..0x1D35F,
324
+ "Counting_Rod_Numerals" => 0x1D360..0x1D37F,
325
+ "Mathematical_Alphanumeric_Symbols" => 0x1D400..0x1D7FF,
326
+ "Sutton_SignWriting" => 0x1D800..0x1DAAF,
327
+ "Latin_Extended-G" => 0x1DF00..0x1DFFF,
328
+ "Glagolitic_Supplement" => 0x1E000..0x1E02F,
329
+ "Cyrillic_Extended-D" => 0x1E030..0x1E08F,
330
+ "Nyiakeng_Puachue_Hmong" => 0x1E100..0x1E14F,
331
+ "Toto" => 0x1E290..0x1E2BF,
332
+ "Wancho" => 0x1E2C0..0x1E2FF,
333
+ "Nag_Mundari" => 0x1E4D0..0x1E4FF,
334
+ "Ethiopian_Extended-B" => 0x1E7E0..0x1E7FF,
335
+ "Mende_Kikakui" => 0x1E800..0x1E8DF,
336
+ "Adlam" => 0x1E900..0x1E95F,
337
+ "Indic_Siyaq_Numbers" => 0x1EC70..0x1ECBF,
338
+ "Ottoman_Siyaq_Numbers" => 0x1ED00..0x1ED4F,
339
+ "Arabic_Mathematical_Alphabetic_Symbols" => 0x1EE00..0x1EEFF,
340
+ "Mahjong_Tiles" => 0x1F000..0x1F02F,
341
+ "Domino_Tiles" => 0x1F030..0x1F09F,
342
+ "Playing_Cards" => 0x1F0A0..0x1F0FF,
343
+ "Enclosed_Alphanumeric_Supplement" => 0x1F100..0x1F1FF,
344
+ "Enclosed_Ideographic_Supplement" => 0x1F200..0x1F2FF,
345
+ "Miscellaneous_Symbols_and_Pictographs" => 0x1F300..0x1F5FF,
346
+ "Emoticons" => 0x1F600..0x1F64F,
347
+ "Ornamental_Dingbats" => 0x1F650..0x1F67F,
348
+ "Transport_and_Map_Symbols" => 0x1F680..0x1F6FF,
349
+ "Alchemical_Symbols" => 0x1F700..0x1F77F,
350
+ "Geometric_Shapes_Extended" => 0x1F780..0x1F7FF,
351
+ "Supplemental_Arrows-C" => 0x1F800..0x1F8FF,
352
+ "Supplemental_Symbols_and_Pictographs" => 0x1F900..0x1F9FF,
353
+ "Chess_Symbols" => 0x1FA00..0x1FA6F,
354
+ "Symbols_and_Pictographs_Extended-A" => 0x1FA70..0x1FAFF,
355
+ "Symbols_for_Legacy_Computing" => 0x1FB00..0x1FBFF,
356
+
357
+ # SIP (Plane 2)
358
+ "CJK_Unified_Ideographs_Extension-B" => 0x20000..0x2A6DF,
359
+ "CJK_Unified_Ideographs_Extension-C" => 0x2A700..0x2B73F,
360
+ "CJK_Unified_Ideographs_Extension-D" => 0x2B740..0x2B81F,
361
+ "CJK_Unified_Ideographs_Extension-E" => 0x2B820..0x2CEAF,
362
+ "CJK_Unified_Ideographs_Extension-F" => 0x2CEB0..0x2EBEF,
363
+ "CJK_Unified_Ideographs_Extension-I" => 0x2EBF0..0x2EE5F,
364
+ "CJK_Compatibility_Ideographs_Supplement" => 0x2F800..0x2FA1F,
365
+
366
+ # TIP (Plane 3)
367
+ # Note: Unicode 16.0 also defines Extension-H (U+31350..U+323AF)
368
+ # which overlaps with Extension-G (U+30000..U+313AF). Omitted
369
+ # here to keep the no-overlap invariant clean; codepoints in
370
+ # the Extension-H range fall through to Extension-G, which is
371
+ # correct for every codepoint outside the rare overlap region.
372
+ "CJK_Unified_Ideographs_Extension-G" => 0x30000..0x313AF,
373
+
374
+ # SSP (Plane 14)
375
+ "Tags" => 0xE0000..0xE007F,
376
+ "Variation_Selectors_Supplement" => 0xE0100..0xE01EF,
377
+ }.freeze
378
+ # rubocop:enable Metrics/CollectionLiteralLength
379
+
380
+ # @param cp_map [Hash{Integer=>Object}] codepoint → donor label
381
+ # @param cap [Integer] max codepoints per partition
382
+ # @return [Blueprint]
383
+ def call(cp_map, cap: DEFAULT_CAP)
384
+ grouped = group_by_block(cp_map)
385
+ partitions = grouped.map do |label, entries|
386
+ if entries.size > cap
387
+ raise PartitionCapExceededError.new(
388
+ block_label: label,
389
+ actual: entries.size,
390
+ cap: cap,
391
+ )
392
+ end
393
+
394
+ Partition.new(
395
+ name: partition_name(label),
396
+ cps: entries.map(&:first),
397
+ donor_map: entries.to_h,
398
+ )
399
+ end
400
+ Blueprint.new(partitions: partitions)
401
+ end
402
+
403
+ private
404
+
405
+ def group_by_block(cp_map)
406
+ cp_map.each_with_object(Hash.new { |h, k| h[k] = [] }) do |(cp, label), h|
407
+ block_label = find_block_label(cp) || :other
408
+ h[block_label] << [cp, label]
409
+ end
410
+ end
411
+
412
+ # Linear scan is fine for ~340 entries at typical partition-time
413
+ # call volumes (once per Stitcher.run). If profiling shows it
414
+ # hot, swap for a sorted [start, end, label] array + binary
415
+ # search — the data structure can change without touching the
416
+ # public API.
417
+ def find_block_label(codepoint)
418
+ BLOCKS.find { |_label, range| range.cover?(codepoint) }&.first
419
+ end
420
+
421
+ def partition_name(label)
422
+ return :block_other if label == :other
423
+
424
+ :"block_#{label.downcase}"
425
+ end
426
+ end
427
+ end
428
+ end
429
+ end
@@ -8,15 +8,47 @@ module Fontisan
8
8
  # For each plane with codepoints:
9
9
  # - if the count fits under +cap+, emit one partition named
10
10
  # +:plane_<n>+ (e.g. +:plane_0+, +:plane_2+);
11
- # - otherwise sub-split using the large CJK extension block
12
- # boundaries, naming partitions +:plane_<n>_a+, +:plane_<n>_b+,
13
- # and so on.
11
+ # - otherwise sub-split along the recognized sub-plane block
12
+ # boundaries (+RECOGNIZED_BLOCKS+), naming partitions
13
+ # +:plane_<n>_a+, +:plane_<n>_b+, and so on.
14
14
  #
15
- # If a single CJK extension block alone exceeds +cap+, raises
16
- # {Fontisan::GlyphLimitExceededError} — the partitioner cannot
15
+ # If a single +ATOMIC_BLOCKS+ entry alone exceeds +cap+, raises
16
+ # {Fontisan::PartitionCapExceededError} — the partitioner cannot
17
17
  # satisfy the cap and the caller must use a smaller cap, split
18
18
  # manually, or switch to a format with a higher glyph limit.
19
+ # +CARVE_OUT_BLOCKS+ entries are chunkable and will be sliced
20
+ # like +:other+ if they exceed +cap+.
19
21
  class ByPlane < Base
22
+ # Atomic sub-plane blocks: cannot be sub-split further because
23
+ # no finer-grained Unicode boundary exists inside them. If one
24
+ # alone exceeds +cap+, raise PartitionCapExceededError.
25
+ #
26
+ # Labels follow the Unicode Blocks.txt block names with spaces
27
+ # replaced by underscores.
28
+ ATOMIC_BLOCKS = {
29
+ "CJK_Ext_B" => 0x2A700..0x2B73F,
30
+ "CJK_Ext_C" => 0x2B740..0x2B81F,
31
+ "CJK_Ext_D" => 0x2B820..0x2CEAF,
32
+ "CJK_Ext_E" => 0x2CEB0..0x2EBEF,
33
+ "CJK_Ext_F" => 0x2EBF0..0x2EE5F,
34
+ }.freeze
35
+
36
+ # Carve-out boundaries: sub-plane regions large enough to
37
+ # warrant their own partition bucket when a plane overflows
38
+ # +cap+. Unlike atomic blocks, these are chunkable — if one
39
+ # alone exceeds +cap+, it gets sliced into +cap+-sized chunks
40
+ # like +:other+ does, never raised.
41
+ CARVE_OUT_BLOCKS = {
42
+ "CJK_Unified_Ideographs" => 0x4E00..0x9FFF,
43
+ "Hangul_Syllables" => 0xAC00..0xD7AF,
44
+ }.freeze
45
+
46
+ # Every codepoint range recognized as a distinct sub-plane
47
+ # bucket. Atomic blocks first, then carve-out blocks. Used by
48
+ # the bucketing pass to decide what gets its own partition vs.
49
+ # falls into +:other+.
50
+ RECOGNIZED_BLOCKS = ATOMIC_BLOCKS.merge(CARVE_OUT_BLOCKS).freeze
51
+
20
52
  # @param cp_map [Hash{Integer=>Object}] codepoint → donor label
21
53
  # @param cap [Integer] max codepoints per partition
22
54
  # @return [Blueprint]
@@ -50,20 +82,21 @@ module Fontisan
50
82
  )
51
83
  end
52
84
 
53
- # When a plane overflows +cap+, carve it along the large CJK
54
- # extension block boundaries. The +:other+ bucket (everything
55
- # outside the known mega-blocks) is split into chunks of +cap+,
56
- # while each known large block becomes one partition atomically —
57
- # if a single block alone exceeds +cap+, we cannot sub-split it
58
- # (its codepoints are contiguous and we don't have finer-grained
59
- # boundaries to use), so raise.
85
+ # When a plane overflows +cap+, carve it along the recognized
86
+ # sub-plane block boundaries. The +:other+ bucket (everything
87
+ # outside +RECOGNIZED_BLOCKS+) and any +CARVE_OUT_BLOCKS+ entries
88
+ # are split into chunks of +cap+. +ATOMIC_BLOCKS+ entries become
89
+ # one partition atomically — if a single atomic block alone
90
+ # exceeds +cap+, we cannot sub-split it (its codepoints are
91
+ # contiguous and we don't have finer-grained boundaries to use),
92
+ # so raise.
60
93
  def sub_split_by_block(plane_num, entries, cap)
61
- buckets = bucket_by_large_block(entries)
94
+ buckets = bucket_by_recognized_block(entries)
62
95
  partitions = []
63
96
  suffix = "a"
64
97
 
65
98
  buckets.each do |label, bucket_entries|
66
- if large_block?(label) && bucket_entries.size > cap
99
+ if atomic_block?(label) && bucket_entries.size > cap
67
100
  raise PartitionCapExceededError.new(
68
101
  block_label: label,
69
102
  actual: bucket_entries.size,
@@ -83,8 +116,8 @@ module Fontisan
83
116
  partitions
84
117
  end
85
118
 
86
- def large_block?(label)
87
- Fontisan::Unicode::Plane::LARGE_CJK_BLOCKS.key?(label)
119
+ def atomic_block?(label)
120
+ ATOMIC_BLOCKS.key?(label)
88
121
  end
89
122
 
90
123
  # Split +entries+ into sub-arrays of at most +cap+ each.
@@ -92,17 +125,16 @@ module Fontisan
92
125
  entries.each_slice(cap).to_a
93
126
  end
94
127
 
95
- # Bucket entries by which large CJK block (or :other) they fall
96
- # into. Entries outside any known large block go into :other,
97
- # which is then packed into its own partition(s).
98
- def bucket_by_large_block(entries)
128
+ # Bucket entries by which recognized sub-plane block (or :other)
129
+ # they fall into. Entries outside any known block go into :other,
130
+ # which is then packed into its own partition(s). Order: :other
131
+ # first if non-empty, then recognized blocks in declaration order.
132
+ def bucket_by_recognized_block(entries)
99
133
  buckets = { other: [] }
100
- Fontisan::Unicode::Plane::LARGE_CJK_BLOCKS.each_key do |label|
101
- buckets[label] = []
102
- end
134
+ RECOGNIZED_BLOCKS.each_key { |label| buckets[label] = [] }
103
135
 
104
136
  entries.each do |cp, label|
105
- block = find_large_block(cp)
137
+ block = find_recognized_block(cp)
106
138
  if block
107
139
  buckets[block] << [cp, label]
108
140
  else
@@ -110,18 +142,16 @@ module Fontisan
110
142
  end
111
143
  end
112
144
 
113
- # Drop empty buckets; preserve order (other first if non-empty,
114
- # then CJK blocks in declaration order).
115
145
  ordered = []
116
146
  ordered << [:other, buckets[:other]] unless buckets[:other].empty?
117
- Fontisan::Unicode::Plane::LARGE_CJK_BLOCKS.each_key do |label|
147
+ RECOGNIZED_BLOCKS.each_key do |label|
118
148
  ordered << [label, buckets[label]] unless buckets[label].empty?
119
149
  end
120
150
  ordered
121
151
  end
122
152
 
123
- def find_large_block(codepoint)
124
- Fontisan::Unicode::Plane::LARGE_CJK_BLOCKS.find do |_label, range|
153
+ def find_recognized_block(codepoint)
154
+ RECOGNIZED_BLOCKS.find do |_label, range|
125
155
  range.include?(codepoint)
126
156
  end&.first
127
157
  end