Skip to content

Add support for merging overlapping scripts - #268

Merged
garretrieger merged 20 commits into
w3c:mainfrom
garretrieger:overlapping_scripts
Oct 5, 2026
Merged

garretrieger merged 20 commits into
w3c:mainfrom
garretrieger:overlapping_scripts

Conversation

@garretrieger

@garretrieger garretrieger commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Previously discussed here: https://github.com/w3c/ift-encoder/blob/main/docs/segmenter.md#multiple-frequency-data-sets-and-merge-groups

Fixes: #261 and supports w3c/ift-encoder-data#8

The approach:

  • Merge groups are updated to support specifying multiple frequency data sets.
  • As previously, code points which are shared across multiple merge groups are moved to a shared group and processed with heuristic merging, however within a merge group the merger algorithm has been updated to support evaluating against more than one frequency data set.
  • The cost function the merger optimizes against is the sum of the cost evaluated against each frequency data set independently. So the merger gives equal importance to each frequency data set.
  • The AutoSegmenterConfig module has been updated to utilize the new merge group capabilities.
  • It will automatically group overlapping scripts into a single merge group. So for example with a CJK font the default behaviour will produce a merge group containing TC, SC, KR and JP frequency data.
  • If a primary language or script is provided then in the case of overlapping scripts one merge group will be produced for the primary script, and a second group will be added which contains all remaining overlapping scripts. Which means for codepoints covered by the primary script the merger will use only the primary scripts frequency data. Any codepoints covered by the overlapping script and not primary script will be processed using multiple frequency data sets.
  • For example in a CJK font with primary script JP there would be two merge groups {JP} and {SC, TC, KR}. Where the {SC, TC, KR} covers only codepoints not found in the {JP} group.
  • As before setting a primary language or script opts that script into eligibility for merging with the initial font.

As an example here is a table of per script final cost after merging with and without a primary script specified on Noto Sans JP:

  SC TC JP KR
No Primary 181,924 276,145 212,887 11,140
JP Primary 237,762 339,807 168,897 19,842

When JP is selected as the primary script the JP final cost is much lower then in the no primary case, however the cost of this is that the cost for the other three scripts are increased relative to the no primary case.

@skef

skef commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

I'm taking a look at this "with help", which adds the caveat that I don't really know what I'm talking about. So I'll first check our understanding:

  1. This satisfies the equivalent of most of the relevant ideas expressed in some_hypo_seg_principles.md in a bottom-up way, using a cost function, that that document specified in terms of top-down heuristics.
  2. The idea of not optimizing the codepoints for an "incomplete" script (a set of codepoints known to map to a given script without their being enough overall codepoints to plausibly support that script) is not implemented.

Anyway, to the extent I'm getting this right I have very favorable views about the PR. One thing I was wondering was whether it might be worth plumbing through an exception to min_group_size for high frequency groupings, so that e.g. if there are only three codepoints that are high frequency in exactly {JP,SC} they don't get merged with others, the rationale being that high frequency codepoints don't have the same privacy implications.

@garretrieger

Copy link
Copy Markdown
Contributor Author

I'm taking a look at this "with help", which adds the caveat that I don't really know what I'm talking about. So I'll first check our understanding:

  1. This satisfies the equivalent of most of the relevant ideas expressed in some_hypo_seg_principles.md in a bottom-up way, using a cost function, that that document specified in terms of top-down heuristics.

Just reviewed that doc and yeah the cost function ends up naturally doing much of what's talked about in there.

  1. The idea of not optimizing the codepoints for an "incomplete" script (a set of codepoints known to map to a given script without their being enough overall codepoints to plausibly support that script) is not implemented.

That's right, at the moment we use a simple check to identify which scripts a font supports which is: if there is at least one codepoint unique to the script which is found in the font, then we treat the script as supported (relevant code: https://github.com/w3c/ift-encoder/blob/main/ift/config/auto_segmenter_config.cc#L248). Once a script is detected then we will do all of the usual merging optimization for it. I opted for an overly conservative 1 codepoint threshold for now, but I think we'll want to raise that up higher. That would have the effect of disabling optimization for incomplete scripts. I'll need to do a bit of research and figure out a reasonable approach to the threshold.

Anyway, to the extent I'm getting this right I have very favorable views about the PR. One thing I was wondering was whether it might be worth plumbing through an exception to min_group_size for high frequency groupings, so that e.g. if there are only three codepoints that are high frequency in exactly {JP,SC} they don't get merged with others, the rationale being that high frequency codepoints don't have the same privacy implications.

That makes sense, I've opened an issue for it: #269

…tors.

Retains existing interface while remainder of the code is migrated to support multiple probability calculators per strategy.
Also reworks how MergeStrategy's are constructed. Cost based now takes probability profile + added unigram/bigram constructors of probability profiles.
… per group.

SegmenterConfigUtil is updated to handle the new fields. The pre-existing fields for specifying frequency data are left, but marked as deprecated. Mixing legacy and the new fields is an error.
When deciding if two scripts overlap we look at the probability weight of overlapping codepoints and ignore overlaps below a threshold. This avoids pulling together too many scripts that have small inconsequential overlaps into a single merge group.
…n merge group.

When a primary script has been requested we should aim to optimize the segmentation for that script, so if the primary script is sharing a merge group with other scripts it will be moved into it's own merge group. The left over scripts in the group will be given coverage of any codepoints not covered by the primary script.
… script.

Previously this defaulted to Script_latin if not explicitly set.
This time the cache is located with the probability calculator instances. Modifies probability calculator API so that segment indices can be optionally supplied + cache invalidation triggers.
@garretrieger

Copy link
Copy Markdown
Contributor Author

Couple of things that I'll need to do before this is ready:

  1. May need some further performance optimizations, particularly for bigram mode in which probability calculations currently take up a large amount of total time. For CJK fonts without a primary script this change multiplies the total number of probability calculations performed by ~4x. I have some ideas here that I need to explore. Unigram mode looks to be in good shape, performance is comparable with with main.
  2. Batch testing: I'd like to run segmentation cost comparisons across a collection of CJK fonts that compare the per CJK script final cost with and without multi script support.

@garretrieger

garretrieger commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor Author

Another data point:

Primary Script Quality SC TC JP KR Total Note
Unified CJK 2 203,674 325,008 236,350 16,048 821,980 Existing Behaviour
None 2 181,841 276,016 212,808 11,139 705,807 Multi Script
JP 2 237,762 339,807 168,897 19,842 829,954 Single Script

This shows a cost comparison to the existing behaviour of segmenting against a single Unified CJK script vs using a multi script cost function from this PR. We see signficant accross the board cost improvements using the new multi script cost function. Still need to run a similar analysis across a larger collection of fonts.

As before targetting a single primary script improves performance on that script at the cost of the others.

@garretrieger

Copy link
Copy Markdown
Contributor Author

Additional issue that I'll need to fix: table keyed segments are currently derived from the merge groups. With this change you can get multiple scripts placed into a single merge group which would result in one large table keyed segment instead of one per script. Will need to rework automatic table keyed segment generation to account for this.

Will likely want to have auto config emit explicit table keyed segments if scripts have been merged into a single merge group instead of relying on the generate_table_keyed_segments mechanism.

…t with the base segment.

Previously CollectCompositeCandidateMerges() would check all composite conditions for merge with base. The vast majority of these are not competive merges but are rechecked on each iteration. This change limits candidates to only those which include base segment, which are more likely to produce cost improvements.

Tested against the Google Fonts corpus. Overall showed fairly consistent speedups and cost reductions on average. For fonts with large composite condition sets drastic speedups are observed.

Additionally this adds a de-duplication step to composite candidate collection which avoids rechecking equivalent merges (including skipping segment pair merges previously checked during CollectExclusiveCandidateMerges).
In the merged bigram probability computation we are computing the probability of a new segment resulting from merging two or more existing segments. Previously this was done by generating the merged segment's definition and computing the probability from scratch, which involves computing all codepoint pair probabilities.

This reworks the merged probability computation:
- Per segment probability calculations now cache the various intermediate sums needed to compute Kounias bounds for that segment.
- Then in the merged computation the previously cached intermediate sums are used as a starting point and augmented with any missing inter-segment codepoint pairs.
- Ultimately this means we need to only deal with inter segment code point pairs and can skip all intra segment code point pairs.

Gives moderate speedups to probability calculations for quality levels 3 and above (those using bigrams).
…n the candidate delta.

Modify ComputeCostDelta() so that all of the positive delta contributions are added up last. Then we can stop iteration early as soon as the inprogress delta exceeds to current best delta.

Since the inprogress sum is done in non-deterministic order we require the inprogress sum exceed the best delta by more than the maximum amount of floating point error. This maintains overall determinism.

In tests w/ bigram probability on a CJK font gave a ~14% speedup.
Instead of using merge groups which may be the result of merging multiple input scripts, construct them from the input frequency data sets. Deprecates segmenter config 'generate_table_keyed_segments' and adds 'generate_table_keyed_segments_mode'  which has a new third mode FROM_FREQ_DATA.
@garretrieger

Copy link
Copy Markdown
Contributor Author

Got results back from batch testing using the unified CJK script vs multi script cost calculation on CJK fonts. It shows cost improvements for nearly all families. Cost delta distribution:

  • 5th percentile -22%
  • Median -5%
  • 95th percentile +1%

As expected there is increased segmentation time for using multi script (though a few cases got faster). Segmentation time distribution:

  • 5th percentile -61%
  • Median +36%
  • 95th percentile +148%

Both runs use the optimizations I just added, so this is a comparison only between single vs multi script.

It now estimates how many table keyed patches will be produced by a config and configures max depth if necessary to keep the total count below a threshold.
@garretrieger
garretrieger marked this pull request as ready for review October 1, 2026 23:12
- Move merger ReassignInitSubset() to after fallback move to init font. Since that also affects the init font definition.
- Don't mix live segment codepoints with cached segment information.
- In calculator_storage use unique_ptr's so we don't have to worry about resize invalidating pointer addresses.
- Use post segmentation initial segment instead of pre when constructing table keyed segments.
- Handle case where primary script has not intersection with font.
@garretrieger
garretrieger merged commit 74d6a4c into w3c:main Oct 5, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Better support for overlapping scripts (eg. CJK) in the segmenter

2 participants