fontisan 0.4.10 → 0.4.12

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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +27 -1
  3. data/Rakefile +53 -39
  4. data/docs/AFDKO_MIGRATION.adoc +175 -0
  5. data/docs/FEATURE_PARITY.adoc +263 -0
  6. data/docs/STITCHER_GUIDE.adoc +100 -6
  7. data/docs/TTX.adoc +176 -0
  8. data/docs/UFO_COMPILATION.adoc +281 -3
  9. data/lib/fontisan/stitcher/partition_strategy/by_block.rb +429 -0
  10. data/lib/fontisan/stitcher/partition_strategy/by_script.rb +305 -0
  11. data/lib/fontisan/stitcher/partition_strategy.rb +2 -0
  12. data/lib/fontisan/stitcher/source.rb +11 -7
  13. data/lib/fontisan/subset/table_subsetter.rb +227 -11
  14. data/lib/fontisan/svg/standalone_glyph.rb +129 -0
  15. data/lib/fontisan/svg.rb +1 -0
  16. data/lib/fontisan/tasks/fixture_downloader.rb +162 -0
  17. data/lib/fontisan/tasks.rb +11 -0
  18. data/lib/fontisan/ufo/cli.rb +34 -21
  19. data/lib/fontisan/ufo/compile/feature_writers/base.rb +56 -0
  20. data/lib/fontisan/ufo/compile/feature_writers/curs.rb +72 -0
  21. data/lib/fontisan/ufo/compile/feature_writers/gdef.rb +90 -0
  22. data/lib/fontisan/ufo/compile/feature_writers/kern.rb +51 -0
  23. data/lib/fontisan/ufo/compile/feature_writers/kern2.rb +57 -0
  24. data/lib/fontisan/ufo/compile/feature_writers/mark.rb +45 -0
  25. data/lib/fontisan/ufo/compile/feature_writers/mark_family_base.rb +122 -0
  26. data/lib/fontisan/ufo/compile/feature_writers/mkmk.rb +53 -0
  27. data/lib/fontisan/ufo/compile/feature_writers.rb +37 -0
  28. data/lib/fontisan/ufo/compile/filters/propagate_anchors.rb +102 -0
  29. data/lib/fontisan/ufo/compile/filters/remove_overlaps.rb +94 -0
  30. data/lib/fontisan/ufo/compile/filters/sort_contours.rb +69 -0
  31. data/lib/fontisan/ufo/compile/filters/transformations.rb +113 -0
  32. data/lib/fontisan/ufo/compile/filters.rb +12 -0
  33. data/lib/fontisan/ufo/compile.rb +1 -0
  34. data/lib/fontisan/ufo/convert/to_dfont.rb +41 -0
  35. data/lib/fontisan/ufo/convert/to_otc.rb +37 -0
  36. data/lib/fontisan/ufo/convert/to_otf.rb +18 -0
  37. data/lib/fontisan/ufo/convert/to_otf2.rb +20 -0
  38. data/lib/fontisan/ufo/convert/to_postscript.rb +59 -0
  39. data/lib/fontisan/ufo/convert/to_ttc.rb +35 -0
  40. data/lib/fontisan/ufo/convert/to_ttf.rb +21 -0
  41. data/lib/fontisan/ufo/convert/to_woff.rb +56 -0
  42. data/lib/fontisan/ufo/convert/to_woff2.rb +45 -0
  43. data/lib/fontisan/ufo/convert.rb +68 -5
  44. data/lib/fontisan/ufo/transformation.rb +11 -0
  45. data/lib/fontisan/version.rb +1 -1
  46. data/lib/fontisan.rb +1 -0
  47. metadata +32 -2
@@ -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