This is an automated email from the ASF dual-hosted git repository.
mawiesne pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opennlp.git
The following commit(s) were added to refs/heads/main by this push:
new 46d9d96a1 GH-1456: Tighten normalizer, tokenizer, introduction and
language detector chapters (#1457)
46d9d96a1 is described below
commit 46d9d96a10570bdd69523c92303e00936a49eab4
Author: Richard Zowalla <[email protected]>
AuthorDate: Sat Oct 10 11:07:54 2026 +0200
GH-1456: Tighten normalizer, tokenizer, introduction and language detector
chapters (#1457)
---
opennlp-docs/src/docbkx/introduction.xml | 47 ++++++++-----------
opennlp-docs/src/docbkx/langdetect.xml | 13 +++---
opennlp-docs/src/docbkx/namefinder.xml | 2 +-
opennlp-docs/src/docbkx/normalizer.xml | 80 +++++++++++++++++---------------
opennlp-docs/src/docbkx/tokenizer.xml | 63 +++++++++++++------------
5 files changed, 100 insertions(+), 105 deletions(-)
diff --git a/opennlp-docs/src/docbkx/introduction.xml
b/opennlp-docs/src/docbkx/introduction.xml
index 93dc7f286..109679a95 100644
--- a/opennlp-docs/src/docbkx/introduction.xml
+++ b/opennlp-docs/src/docbkx/introduction.xml
@@ -318,18 +318,17 @@ Arguments description:
<section xml:id="intro.stream.resources">
<title>Closing and resetting input streams</title>
<para>
- Close corpus streams after use, preferably with
try-with-resources. Since 3.0.0,
+ Close corpus streams after use, preferably with
try-with-resources.
<code>PlainTextByLineStream.reset()</code> closes the
reader from the previous
pass before asking its <code>InputStreamFactory</code>
for a new stream. The
- factory must therefore provide a fresh, readable stream
for each pass, rather
+ factory must therefore provide a new, readable stream
for each pass, rather
than return the stream that has just been closed.
</para>
<para>
The markable file stream that the command line tools
and the format converters open
for corpus files releases its file handle on close,
including when closed through
a wrapping reader. The Leipzig language-sample reader
also
- closes the stream used to count source lines. These
fixes prevent file handles
- from accumulating during repeated reads and training
passes.
+ closes the stream used to count source lines.
</para>
</section>
<section xml:id="intro.sysprops">
@@ -355,38 +354,30 @@ Arguments description:
<section xml:id="intro.sysprops.whitespace">
<title>Whitespace Definition</title>
<para>
- What counts as whitespace is a common interoperability gap
across programming
- languages and libraries. The Unicode Character Database
defines a binary
- <code>White_Space</code> property. As documented on
- <link
xlink:href="https://en.wikipedia.org/wiki/Whitespace_character">Wikipedia's
- whitespace character page</link>, Unicode defines exactly
twenty-five characters
- with <code>White_Space=yes</code> (<quote>WSpace=Y</quote> /
<quote>WS</quote>),
- including ordinary space and tab, the next-line control
(<code>U+0085</code>),
- no-break and typographic spaces such as <code>U+00A0</code> and
- <code>U+2007</code>, and the line and paragraph separators
- (<code>U+2028</code>/<code>U+2029</code>). That page also
states the cross-language
- weakness directly: <quote>Most languages only recognize
whitespace characters that
- have an ASCII code. They disallow most or all of the Unicode
codes listed
- above</quote>. Java is a concrete example of that mismatch:
+ The Unicode standard defines whitespace as the 25 code points
with the binary
+ <code>White_Space</code> property
+ (<link
xlink:href="https://www.unicode.org/Public/17.0.0/ucd/PropList.txt"><code>PropList.txt</code></link>,
+ Unicode 17.0.0). The set includes space and tab, the next-line
control
+ (<code>U+0085</code>), no-break and typographic spaces such as
<code>U+00A0</code>
+ and <code>U+2007</code>, and the line and paragraph separators
+ (<code>U+2028</code>/<code>U+2029</code>). Java's
<code>Character.isWhitespace</code> omits the no-break spaces
and
- <code>U+0085</code> while including the information separators
- <code>U+001C</code>..<code>U+001F</code>, and Java's default
<code>\s</code> regex
- class is likewise an ASCII-oriented subset unless Unicode
character-class mode is
- enabled. Text extracted from PDFs, HTML, and word processors
regularly carries the
- wider set, so a tokenizer that follows the JVM predicate and
one that follows
- Unicode will disagree on those documents.
+ <code>U+0085</code> but includes the information separators
+ <code>U+001C</code>..<code>U+001F</code>; Java's default
<code>\s</code> regex
+ class matches ASCII whitespace only unless Unicode
character-class mode is
+ enabled. Text extracted from PDFs, HTML, and word processors
often contains the
+ wider set, on which the two definitions produce different
tokens.
</para>
<para>
Tokenization, corpus format parsing, and feature generation
therefore classify
whitespace using the Unicode <code>White_Space</code> property
by default, via
<code>StringUtil.isWhitespace</code> and the related
<code>StringUtil.splitOnUnicodeWhitespace</code> /
- <code>trimUnicodeWhitespace</code> helpers (code-point scans,
not regular
- expressions). Models trained with OpenNLP 1.x or 2.x used the
legacy definition
+ <code>trimUnicodeWhitespace</code> helpers. Models trained
with OpenNLP 1.x or
+ 2.x, which include the models published by the project, used
the legacy definition
(the union of <code>Character.isWhitespace</code> and the
Unicode <code>Zs</code>
- category). As of this release, that covers all published
OpenNLP models, so set
- the property below when decoding those models if you need
byte-identical
- tokenization at the few code points where the two definitions
differ.
+ category). Set the property below when applying those models
to reproduce their
+ tokenization at the code points where the two definitions
differ.
</para>
<para>
Users may switch to the legacy definition by setting the
following system
diff --git a/opennlp-docs/src/docbkx/langdetect.xml
b/opennlp-docs/src/docbkx/langdetect.xml
index dbc295a33..cb78406e4 100644
--- a/opennlp-docs/src/docbkx/langdetect.xml
+++ b/opennlp-docs/src/docbkx/langdetect.xml
@@ -58,16 +58,17 @@ under the License.
<row>
<entry>UrlCharSequenceNormalizer</entry>
<entry>Replaces each
http or https URL and each email address with a
- blank space.
Since 3.0.0 a URL is removed as a whole, up to the next
+ blank space. A
URL is removed as a whole, up to the next
whitespace or
delimiter, including a port, user information, percent
escapes and
non-ASCII hosts; URLs with other schemes are kept. Models
- trained with an
earlier release saw only the leading ASCII part of a
+ trained with a
release before 3.0.0 saw only the leading ASCII part of a
URL removed and
get that output again under
<code>-Dopennlp.compat.mode=LEGACY</code> until they are retrained.</entry>
</row>
<row>
-
<entry>TwitterCharSequenceNormalizer</entry>
- <entry>Replaces
hashtags and Twitter usernames by blank spaces.</entry>
+
<entry>SocialMediaCharSequenceNormalizer</entry>
+ <entry>Replaces each
hashtag, user handle, run of retweet markers and emoticon
+ with one space,
and shrinks repeated laughter to two repetitions.</entry>
</row>
<row>
<entry>NumberCharSequenceNormalizer</entry>
@@ -82,8 +83,8 @@ under the License.
</table>
</para>
<para>
- Since 3.0.0, EmojiCharSequenceNormalizer removes
complete emoji only. Models trained with an earlier
- release saw every hyphen, every character from U+E000
to U+FFFF and nearly every supplementary
+ EmojiCharSequenceNormalizer removes complete emoji
only. Models trained with a release before
+ 3.0.0 saw every hyphen, every character from U+E000 to
U+FFFF and nearly every supplementary
character removed, emoji or not. They get that output
again under
<code>-Dopennlp.compat.mode=LEGACY</code> until they
are retrained.
</para>
diff --git a/opennlp-docs/src/docbkx/namefinder.xml
b/opennlp-docs/src/docbkx/namefinder.xml
index 43bbeb58a..359dec9a3 100644
--- a/opennlp-docs/src/docbkx/namefinder.xml
+++ b/opennlp-docs/src/docbkx/namefinder.xml
@@ -185,7 +185,7 @@ Span[] spans = nameFinderDL.findInOriginal(tokens);]]>
<code>White_Space</code> set before
WordPiece tokenization, so spacing such as a
no-break space or the CJK ideographic
space is recognized as a delimiter. After
inference, reconstructed entity text is
matched back to the caller's original input
- with a Unicode-aware cursor scan (not a
regular expression), so
+ with a Unicode-aware cursor scan, so
<code>Span#getCoveredText(...)</code>
returns the source text even when WordPiece
rejoins sub-tokens with spaces or when
the source uses non-ASCII whitespace between
tokens.
diff --git a/opennlp-docs/src/docbkx/normalizer.xml
b/opennlp-docs/src/docbkx/normalizer.xml
index 7950772ee..47661ecd8 100644
--- a/opennlp-docs/src/docbkx/normalizer.xml
+++ b/opennlp-docs/src/docbkx/normalizer.xml
@@ -35,7 +35,9 @@
<para>
<emphasis
role="bold">Standards-sourced.</emphasis> Membership sets come from the
Unicode Character Database (for example
the <code>White_Space</code> and
- <code>Dash</code> properties), not from
the JVM's locale-dependent or quirky
+ <code>Dash</code> properties in
+ <link
xlink:href="https://www.unicode.org/Public/17.0.0/ucd/PropList.txt"><code>PropList.txt</code></link>,
+ Unicode 17.0.0), not from the JVM's
locale-dependent or quirky
character predicates. The normalization
engine never relies on
<code>Character.isWhitespace</code>,
which disagrees with the Unicode standard.
</para>
@@ -158,7 +160,15 @@
<row>
<entry><code>CaseFoldCharSequenceNormalizer</code></entry>
<entry>Lower cases for
case-insensitive matching, using
-
<code>Locale.ROOT</code>.</entry>
+
<code>Locale.ROOT</code> or a supplied locale. Does not apply full case
+ folding; U+00DF is
unchanged.</entry>
+ </row>
+ <row>
+
<entry><code>FullCaseFoldCharSequenceNormalizer</code></entry>
+ <entry>Applies Unicode full
case folding (<code>CaseFolding.txt</code>,
+ status <code>C</code>
and <code>F</code>), including the expanding
+ folds such as U+00DF to
<code>ss</code> and U+FB00 to <code>ff</code>.
+ The Turkic mappings are
not applied. Expects NFC input.</entry>
</row>
<row>
<entry><code>AccentFoldCharSequenceNormalizer</code></entry>
@@ -298,9 +308,8 @@ Span hit = aligned.toOriginalSpan(5, 14); // "the-match"
in the normalized tex
<code>NameFinderDL</code> additionally locates
reconstructed entity text in the original
input with a cursor-based matcher: a space in the
reconstructed span matches zero or more
Unicode whitespace code points in the source, and every
other code point is compared
- case-insensitively. This replaces the previous
regular-expression approach and correctly
- handles spacing copied from PDFs, the web, or non-Latin
sources when resolving
- <code>Span#getCoveredText(...)</code>.
+ case-insensitively. This handles spacing copied from
PDFs, the web, or non-Latin sources
+ when resolving <code>Span#getCoveredText(...)</code>.
</para>
<para>
Optional input folding is controlled through
<code>InferenceOptions</code> and is
@@ -403,7 +412,7 @@ String[] tokens = ws.split("one two"); // ["one", "two"]
(offset-aware via spl
dash.normalize("a—b"); // "a-b"]]>
</programlisting>
<para>
- A class applies its replacement three ways, which
differ in whether they collapse runs and
+ A class applies its replacement in four ways, which
differ in whether they collapse runs and
whether they preserve character offsets:
</para>
<itemizedlist>
@@ -514,20 +523,20 @@ Term term = analyzer.analyze("Running").get(0);
unrequested dimension costs one transform. The
character-level dimensions have built-in
defaults; <code>STEM</code> and <code>LEMMA</code>
require a
<code>Stemmer</code> or <code>Lemmatizer</code> (and
<code>LEMMA</code> a part-of-speech
- tag), and fail loudly if requested without them. An
analyzer is immutable and safe to share
- across threads when its configured transforms are. The
built-in character normalizers are
+ tag); requesting either without them throws an
<code>IllegalStateException</code>.
+ An analyzer is immutable and safe to share across
threads when its configured transforms are. The built-in character normalizers
are
stateless and <code>SnowballStemmer</code> is
thread-safe, so they can be shared as is. A
<code>Stemmer</code> passed to
<code>stem(Stemmer)</code> is used directly, so a stateful one
such as <code>PorterStemmer</code> confines the
analyzer to one thread. Passing a
<code>StemmerFactory</code> to
<code>stem(StemmerFactory)</code> instead wraps it in a
<code>CachingStemmer</code> (see <xref
linkend="tools.stemmer.sharing"/>), which gives each
- thread its own stemmer and cache, so the analyzer is
shareable even when the factory mints
+ thread its own stemmer and cache, so the analyzer is
shareable even when the factory creates
stateful stemmers.
</para>
<para>
Each dimension's transform is configurable on the
builder. Beyond the no-argument methods
- that enable a dimension with its default, there are
convenience methods for the common
- knobs, and a general <code>transform(dimension,
normalizer)</code> escape hatch for any
+ that enable a dimension with its default, there are
convenience methods for common
+ options, and the general method
<code>transform(dimension, normalizer)</code> for any
character-level dimension:
</para>
<programlisting language="java">
@@ -551,18 +560,15 @@ Term term = analyzer.analyze("Running").get(0);
<section xml:id="tools.normalizer.emojiannotations">
<title>Emoji annotations</title>
<para>
- Beyond folding, a pictograph carries signal worth
keeping: a name, a coarse sentiment, an
- entity type, a document category, and for flags a
region. <code>EmojiAnnotator</code> is the
- opt-in surface for that signal. It assembles one
<code>EmojiAnnotation</code> record per
- symbol from three strictly separated layers:
<emphasis>bundled facts</emphasis> from the
- provenance-tagged, project-authored
<code>emoji-annotations.txt</code> table,
- <emphasis>derived facts</emphasis> computed from the
code point sequence itself
- (<code>EmojiFlags</code> decodes a regional indicator
pair to its ISO 3166-1 alpha-2 code and
- a subdivision tag sequence to its ISO 3166-2 code, with
no data file), and optional
- <emphasis>joined facts</emphasis> resolved at run time
through an
- <code>EmojiAnnotationJoin</code> hook against
user-installed data such as a gazetteer.
- Every attribute value carries its own provenance
source, and gazetteer identifiers are never
- baked into the bundled table.
+ <code>EmojiAnnotator</code> provides optional metadata
for a pictograph: a name, a coarse
+ sentiment, an entity type, a document category and, for
flags, a region. It assembles one
+ <code>EmojiAnnotation</code> record per symbol from
three separate sources: the bundled,
+ project-authored <code>emoji-annotations.txt</code>
table; values computed from the code
+ point sequence itself (<code>EmojiFlags</code> decodes
a regional indicator pair to its
+ ISO 3166-1 alpha-2 code and a subdivision tag sequence
to its ISO 3166-2 code, with no data
+ file); and optional values resolved at run time through
an <code>EmojiAnnotationJoin</code>
+ hook against user-installed data such as a gazetteer.
Every attribute value records its
+ source. The bundled table contains no gazetteer
identifiers.
</para>
<programlisting language="java">
<![CDATA[EmojiAnnotator annotator = new EmojiAnnotator();
@@ -581,31 +587,29 @@ for (Term term : analyzer.analyze(text)) {
}]]>
</programlisting>
<para>
- <code>EmojiAnnotatorUsageExampleTest</code> asserts the
behavior shown here.
- Annotations are per-symbol
<emphasis>metadata</emphasis>, not text transforms, so they are a
- parallel accessor surface beside <code>Term</code>
rather than additional
- <code>Dimension</code>s: <code>annotate(Term)</code>
reads the token's original layer, which
- is the symbol the author actually wrote even after a
fold dimension has rewritten it to
- ASCII. Annotation is total over arbitrary tokens (a
non-emoji token is simply empty, and
- degenerate flag-shaped text yields no region instead of
an exception), while the strict
- decoder <code>EmojiFlags.isoRegion()</code> fails
loudly on malformed sequences for callers
- that want the contract. The bundled table's sentiment
scores are original project judgments
+ Annotations are per-symbol
<emphasis>metadata</emphasis>, not text transforms, and are
+ therefore not <code>Dimension</code>s:
<code>annotate(Term)</code> reads the token's original
+ layer, the symbol as written, even after a fold
dimension has rewritten it. It accepts any
+ token: a non-emoji token yields an empty
+ result, and malformed flag-shaped text yields no
region. The strict decoder
+ <code>EmojiFlags.isoRegion()</code> throws an
<code>IllegalArgumentException</code> on
+ malformed flag sequences. The bundled table's sentiment
scores are original project judgments
(tagged <code>UNSPECIFIED</code>); its names and its
entity-type/category assignments derive
from the Unicode-licensed CLDR short names and
<code>emoji-test.txt</code> group headers, and
no third-party sentiment data set is included.
</para>
<para>
- The annotations are wired as strictly opt-in feature
inputs to three components, each
- through its existing extension seam so that default
behavior and existing models are
- unchanged: the name finder (and any consumer of feature
generation descriptors) activates
+ The annotations are optional feature inputs to three
components, each added through an
+ existing extension point, so default behavior and
existing models are unchanged: the name
+ finder (and any consumer of feature generation
descriptors) activates
<code>EmojiAnnotationFeatureGenerator</code> by naming
<code>EmojiAnnotationFeatureGeneratorFactory</code> in
a descriptor, the document
categorizer takes <code>EmojiFeatureGenerator</code>
alongside its defaults through the
<code>DoccatFactory</code> feature generator array, and
sentiment analysis trains with
<code>EmojiSentimentFactory</code>, whose context
generator adds the emoji evidence and is
- re-created from the model manifest at prediction time.
A flag token then carries
- gazetteer-like evidence (<code>emojiType=FLAG</code>,
<code>emojiRegion=DE</code>) with no
- dictionary, and a heart or crying face contributes its
coarse polarity directly.
+ re-created from the model manifest at prediction time.
A flag token then yields the
+ features <code>emojiType=FLAG</code> and
<code>emojiRegion=DE</code> without a dictionary,
+ and a heart or crying face contributes its coarse
polarity directly.
</para>
</section>
diff --git a/opennlp-docs/src/docbkx/tokenizer.xml
b/opennlp-docs/src/docbkx/tokenizer.xml
index 34a702429..2eb69fad2 100644
--- a/opennlp-docs/src/docbkx/tokenizer.xml
+++ b/opennlp-docs/src/docbkx/tokenizer.xml
@@ -240,11 +240,10 @@ boolean word = policy.test("Cafe\u0301");]]>
Use <code>WhitespaceTokenizer.newInstance(boolean
keepNewLines)</code> when a
caller needs its own whitespace tokenizer
configuration. Each call returns a
distinct instance. Changing it does not change
- <code>WhitespaceTokenizer.INSTANCE</code>. Since 3.0.0,
- <code>TokenizerME</code> uses private whitespace
tokenizers and no longer changes
- the shared instance when applying its
<code>keepNewLines</code> setting.
- The shared instance remains mutable; callers needing
different settings should
- use separate instances rather than reconfigure the
singleton.
+ <code>WhitespaceTokenizer.INSTANCE</code>.
<code>TokenizerME</code> uses its own
+ whitespace tokenizers and does not change the shared
instance. The shared
+ instance is mutable; callers that need different
settings should use separate
+ instances instead of reconfiguring it.
</para>
</section>
<section xml:id="tools.tokenizer.training">
@@ -525,8 +524,7 @@ DetokenizationDictionary dict = new
DetokenizationDictionary(tokens, operations)
<![CDATA[int[] boundaries = WordSegmenter.boundaries("The quick brown fox.");
List<Span> segments = WordSegmenter.segments("The quick brown fox.");]]>
</programlisting>
- For allocation-free processing of large inputs,
stream the segments to a callback instead
- of collecting them.
+ To process the segments without building a
result list, stream them to a callback.
<programlisting language="java">
<![CDATA[WordSegmenter.forEachSegment("The quick brown fox.", (start, end) -> {
// handle the segment [start, end)
@@ -564,8 +562,7 @@ for (WordToken token : wordTokenizer.tokenizeTyped("OpenNLP
3.0")) {
<code>HANGUL</code>,
<code>SOUTHEAST_ASIAN</code>, and <code>EMOJI</code>.
</para>
<para>
- A streaming overload reports each token to a
handler with no per-token allocation, which
- is the fastest option when the tokens are
consumed on the fly.
+ A streaming overload reports each token to a
handler without building a result list.
<programlisting language="java">
<![CDATA[WordTokenizer wordTokenizer = new WordTokenizer();
wordTokenizer.tokenize("The quick brown fox.", (start, end, type) -> {
@@ -593,10 +590,6 @@ wordTokenizer.tokenize("The quick brown fox.", (start,
end, type) -> {
expected SHA-256 or SHA-512 digest; an
application-supplied opt-in catalog
(<code>installFromCatalog</code>) also needs
<code>-Dopennlp.download.remote=true</code>.
- <code>matrix.def</code> must list a cost for
every declared cell; matrix
- dimensions plus lexicon size are bounded by the
shared
- <code>ResourceLimits.MAX_ENTRIES</code> limit
and the matrix cell count
- by <code>ResourceLimits.MAX_MATRIX_CELLS</code>.
Fetching, verification, and unpacking use
<code>ResourceInstaller</code>,
described in the model loading chapter. Default
limits are a 1 GiB
download, 4 GiB unpacked, and 100000 archive
entries. Set higher limits at
@@ -607,25 +600,6 @@ wordTokenizer.tokenize("The quick brown fox.", (start,
end, type) -> {
<code>opennlp.install.max.expansion.ratio</code> for larger dictionaries such as
UniDic. Installed files are flattened to their
base names. Existing target
files are not replaced and must be removed
before a refresh.
- Dictionary loading rejects empty
lexicon surfaces, duplicate
- <code>matrix.def</code> entries,
duplicate <code>char.def</code>
- categories, costs outside the signed
16-bit range, malformed text in the
- selected encoding, and
<code>unk.def</code> templates for undefined
- categories. MeCab-quoted CSV fields are
accepted, with three documented
- deviations from MeCab's reader: quotes
open and close a quoted stretch
- wherever they appear in a field, text
after a closing quote joins the
- field instead of being dropped, and an
unterminated quoted field is
- rejected with its file and line instead
of being read silently to the
- end of the line.
- A <code>char.def</code> may define up
to 17 categories, and category
- lengths range from 0 through 15,
matching the MeCab character-property
- compiler.
- If a <code>char.def</code> mapping
lists multiple categories, the first
- provides unknown-word settings.
Grouping continues while successive
- assignments overlap. Unknown words use
their category's <code>unk.def</code>
- template first, with
<code>DEFAULT</code> used when no category template exists.
- <code>LatticeUsageExampleTest</code> asserts
the install-load-tokenize and
- lexicon flows shown here.
<programlisting language="java">
<![CDATA[MecabDictionaryInstaller.install(archiveUri, dictionaryDirectory,
sha512);
LatticeTokenizer tokenizer =
@@ -640,6 +614,31 @@ UnigramSegmenter segmenter =
UnigramSegmenter.load(Path.of("chinese-words.txt"))
String[] words = segmenter.tokenize("我来到北京清华大学");]]>
</programlisting>
</para>
+ <para>
+ <code>matrix.def</code> must list a cost for
every declared cell; matrix
+ dimensions plus lexicon size are bounded by the
shared
+ <code>ResourceLimits.MAX_ENTRIES</code> limit
and the matrix cell count
+ by <code>ResourceLimits.MAX_MATRIX_CELLS</code>.
+ Dictionary loading rejects empty lexicon
surfaces, duplicate
+ <code>matrix.def</code> entries, duplicate
<code>char.def</code>
+ categories, costs outside the signed 16-bit
range, malformed text in the
+ selected encoding, and <code>unk.def</code>
templates for undefined
+ categories. MeCab-quoted CSV fields are
accepted, with three
+ deviations from MeCab's reader: quotes open and
close a quoted stretch
+ wherever they appear in a field, text after a
closing quote joins the
+ field instead of being dropped, and an
unterminated quoted field is
+ rejected with its file and line instead of
being read silently to the
+ end of the line.
+ </para>
+ <para>
+ A <code>char.def</code> may define up to 17
categories, and category
+ lengths range from 0 through 15, matching the
MeCab character-property
+ compiler.
+ If a <code>char.def</code> mapping lists
multiple categories, the first
+ provides unknown-word settings. Grouping
continues while successive
+ assignments overlap. Unknown words use their
category's <code>unk.def</code>
+ template first, with <code>DEFAULT</code> used
when no category template exists.
+ </para>
</section>
</section>
<section xml:id="tools.tokenizer.subword">