From f0d25a8a31662b2f55d0f4e61a0448d77d8a20d6 Mon Sep 17 00:00:00 2001 From: Kevin Schaal Date: Fri, 17 Oct 2025 11:02:35 +0200 Subject: [PATCH] feat: regression test --- .vscode/launch.json | 10 +- .vscode/settings.json | 3 + package-lock.json | 10 ++ package.json | 2 +- src/regression-test/main.ts | 114 ++++++++++++++++++ src/regression-test/output/_indexing-meta.txt | 9 ++ .../output/boston-deletion.txt | 22 ++++ .../output/boulder-creek-substitution.txt | 22 ++++ .../output/carcassonne-infix.txt | 22 ++++ .../output/carcassonne-prefix.txt | 22 ++++ .../output/carcassonne-suffix.txt | 22 ++++ .../output/kuwait-city-prefix.txt | 22 ++++ src/regression-test/output/kuwait-city.txt | 22 ++++ .../output/munich-insertion.txt | 22 ++++ .../output/tbilisi-deletion.txt | 22 ++++ src/regression-test/output/tbilisi.txt | 22 ++++ src/regression-test/output/tokyo-prefix.txt | 22 ++++ src/regression-test/output/tokyo.txt | 13 ++ .../output/t\303\274bingen-transposition.txt" | 22 ++++ 19 files changed, 423 insertions(+), 2 deletions(-) create mode 100644 src/regression-test/main.ts create mode 100644 src/regression-test/output/_indexing-meta.txt create mode 100644 src/regression-test/output/boston-deletion.txt create mode 100644 src/regression-test/output/boulder-creek-substitution.txt create mode 100644 src/regression-test/output/carcassonne-infix.txt create mode 100644 src/regression-test/output/carcassonne-prefix.txt create mode 100644 src/regression-test/output/carcassonne-suffix.txt create mode 100644 src/regression-test/output/kuwait-city-prefix.txt create mode 100644 src/regression-test/output/kuwait-city.txt create mode 100644 src/regression-test/output/munich-insertion.txt create mode 100644 src/regression-test/output/tbilisi-deletion.txt create mode 100644 src/regression-test/output/tbilisi.txt create mode 100644 src/regression-test/output/tokyo-prefix.txt create mode 100644 src/regression-test/output/tokyo.txt create mode 100644 "src/regression-test/output/t\303\274bingen-transposition.txt" diff --git a/.vscode/launch.json b/.vscode/launch.json index 77fba74..82d9d50 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,10 +4,18 @@ { "type": "node", "request": "launch", - "name": "Launch Program", + "name": "Launch Basic Usage", "skipFiles": ["/**"], "program": "${workspaceFolder}/dist/basic-usage.js", "outFiles": ["${workspaceFolder}/**/*.js"] + }, + { + "type": "node", + "request": "launch", + "name": "Run Regression Test", + "skipFiles": ["/**"], + "program": "${workspaceFolder}/dist/regression-test/main.js", + "outFiles": ["${workspaceFolder}/**/*.js"] } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index d632a35..d0f2161 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,5 +9,8 @@ "liveServer.settings.port": 5501, "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[plaintext]": { + "editor.renderControlCharacters": false } } diff --git a/package-lock.json b/package-lock.json index cd0a715..2c4ecaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,6 +70,7 @@ "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.0", @@ -3417,6 +3418,7 @@ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -3629,6 +3631,7 @@ "integrity": "sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.15.0", "@typescript-eslint/types": "8.15.0", @@ -3800,6 +3803,7 @@ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4315,6 +4319,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001669", "electron-to-chromium": "^1.5.41", @@ -5412,6 +5417,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -7212,6 +7218,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -9060,6 +9067,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", @@ -10108,6 +10116,7 @@ "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -11317,6 +11326,7 @@ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 745d8a4..97040fa 100644 --- a/package.json +++ b/package.json @@ -74,4 +74,4 @@ "dist", "usage-examples" ] -} \ No newline at end of file +} diff --git a/src/regression-test/main.ts b/src/regression-test/main.ts new file mode 100644 index 0000000..c3a315f --- /dev/null +++ b/src/regression-test/main.ts @@ -0,0 +1,114 @@ +/* + Regression tests. Run this file and check the git diff of the output files to see what changed. +*/ + +import { readFileSync, writeFileSync } from 'fs'; +import { Config } from '../config.js'; +import { EntityMatch } from '../interfaces/entity-match.js'; +import { EntityResult } from '../interfaces/entity-result.js'; +import { Meta } from '../interfaces/meta.js'; +import { Query } from '../interfaces/query.js'; +import { SearcherFactory } from '../searcher-factory.js'; + +const outputPath = './src/regression-test/output'; +const shortColumnWidth = 8; +const wideColumnWidth = 40; +const RLI = '\u2067'; +const LRI = '\u2066'; +const PDI = '\u2069'; + +const text = readFileSync('./data/world-ctvs.txt', 'utf8'); +const lines = text.split('\n').slice(1); +const entities = lines.map((l, index) => ({ id: index, name: l })); + +interface GeoEntity { + id: number; + name: string; +} + +const config = Config.createDefaultConfig(); +config.normalizerConfig.allowCharacter = (_) => true; +const searcher = SearcherFactory.createSearcher(config); + +console.log(`Indexing ${entities.length} entities...`); +const indexingMeta: Meta = searcher.indexEntities( + entities, + (e) => e.id, + (e) => e.name.split(';') +); +writeFileSync(`${outputPath}/_indexing-meta.txt`, metaToJson(indexingMeta), { encoding: 'utf8' }); + +console.log('Running queries...'); + +runQuery('carcassonne-prefix', 'carcasso'); +runQuery('carcassonne-infix', 'cassonn'); +runQuery('carcassonne-suffix', 'sonne'); +runQuery('munich-insertion', 'muniich'); +runQuery('boston-deletion', 'bostn'); +runQuery('boulder-creek-substitution', 'boulder creak'); +runQuery('tübingen-transposition', 'tübignen'); +runQuery('tokyo', '東京都'); +runQuery('tokyo-prefix', '東京'); +runQuery('tbilisi', 'თბილისი'); +runQuery('tbilisi-deletion', 'თბიისი'); +runQuery('kuwait-city', 'مدينة الكويت', true); +runQuery('kuwait-city-prefix', 'مدينة الك', true); + +console.log('Finished.'); + +function runQuery(queryName: string, queryString: string, rtl: boolean = false) { + const query: Query = new Query(queryString); + const result: EntityResult = searcher.getMatches(query); + console.log(`'${queryString}' (${queryName}): ${result.matches.length} matches.`); + const queryJson = JSON.stringify(query, null, 2); + const metaJson = metaToJson(result.meta); + const matchesString = matchesToString(result.matches, rtl); + const output = `${queryJson}\n\n${metaJson}\n\n${matchesString}`; + writeFileSync(`${outputPath}/${queryName}.txt`, output); +} + +function metaToJson(meta: Meta): string { + return JSON.stringify(Object.fromEntries(meta.allEntries), null, 2); +} + +function matchesToString(matches: EntityMatch[], rtl: boolean): string { + const header = + padRight('Rank', shortColumnWidth) + + padRight('Entity', wideColumnWidth) + + padRight('Matched String', wideColumnWidth) + + padRight('Quality', shortColumnWidth) + + '\n\n'; + const matchesString = matches.map((m, i) => matchToString(m, i + 1, rtl)).join('\n'); + return header + matchesString; +} + +function matchToString(match: EntityMatch, rank: number, rtl: boolean): string { + if (!rtl) { + return ( + padRight(rank.toString(), shortColumnWidth) + + padRight(match.entity.name, wideColumnWidth) + + padRight(match.matchedString, wideColumnWidth) + + padRight(match.quality.toFixed(2), shortColumnWidth) + ); + } + return ( + padAndMark(rank.toString(), shortColumnWidth, false) + + padAndMark(match.entity.name, wideColumnWidth, true) + + padAndMark(match.matchedString, wideColumnWidth, true) + + padAndMark(match.quality.toFixed(2), shortColumnWidth, false) + ); +} + +function padAndMark(s: string, targetWidth: number, rtl: boolean): string { + const padded = rtl ? padLeft(s, targetWidth) : padRight(s, targetWidth); + const mark = rtl ? RLI : LRI; + return mark + padded + PDI; +} + +function padRight(s: string, targetWidth: number): string { + return s + ' '.repeat(Math.max(0, targetWidth - s.length)); +} + +function padLeft(s: string, targetWidth: number): string { + return ' '.repeat(Math.max(0, targetWidth - s.length)) + s; +} diff --git a/src/regression-test/output/_indexing-meta.txt b/src/regression-test/output/_indexing-meta.txt new file mode 100644 index 0000000..74697ef --- /dev/null +++ b/src/regression-test/output/_indexing-meta.txt @@ -0,0 +1,9 @@ +{ + "numberOfInvalidTerms": 1, + "numberOfDistinctTerms": 1190185, + "normalizationDuration": 1342, + "numberOfSurrogateCharacters": 46, + "numberOfEntities": 1237154, + "numberOfTerms": 1237486, + "indexingDuration": 6741 +} \ No newline at end of file diff --git a/src/regression-test/output/boston-deletion.txt b/src/regression-test/output/boston-deletion.txt new file mode 100644 index 0000000..e79d565 --- /dev/null +++ b/src/regression-test/output/boston-deletion.txt @@ -0,0 +1,22 @@ +{ + "string": "bostn", + "topN": 10, + "minQuality": 0.3 +} + +{ + "queryDuration": 2 +} + +Rank Entity Matched String Quality + +1 Bosti Bosti 0.67 +2 Bosto Bosto 0.67 +3 Bosta Bosta 0.67 +4 Bost Bost 0.63 +5 Boston Boston 0.54 +6 Boston Boston 0.54 +7 Boston Boston 0.54 +8 Boston Boston 0.54 +9 Bostel Bostel 0.54 +10 Bostan Bostan 0.54 \ No newline at end of file diff --git a/src/regression-test/output/boulder-creek-substitution.txt b/src/regression-test/output/boulder-creek-substitution.txt new file mode 100644 index 0000000..5ea8a03 --- /dev/null +++ b/src/regression-test/output/boulder-creek-substitution.txt @@ -0,0 +1,22 @@ +{ + "string": "boulder creak", + "topN": 10, + "minQuality": 0.3 +} + +{ + "queryDuration": 2 +} + +Rank Entity Matched String Quality + +1 Boulder Creek Boulder Creek 0.75 +2 Boulder City Boulder City 0.61 +3 Boulder Boulder 0.54 +4 Bouldercombe Bouldercombe 0.54 +5 South Boulder South Boulder 0.54 +6 Boulder Boulder 0.54 +7 Boulder Hill Boulder Hill 0.54 +8 The Boulders The Boulders 0.47 +9 Boulders Boulders 0.47 +10 Boulder Junction Boulder Junction 0.45 \ No newline at end of file diff --git a/src/regression-test/output/carcassonne-infix.txt b/src/regression-test/output/carcassonne-infix.txt new file mode 100644 index 0000000..50596ad --- /dev/null +++ b/src/regression-test/output/carcassonne-infix.txt @@ -0,0 +1,22 @@ +{ + "string": "cassonn", + "topN": 10, + "minQuality": 0.3 +} + +{ + "queryDuration": 4 +} + +Rank Entity Matched String Quality + +1 Cassone Cassone 0.75 +2 Casson Casson 0.71 +3 Cassou Cassou 0.59 +4 Cassola Cassola 0.59 +5 Cassop Cassop 0.59 +6 Canossa Canossa 0.59 +7 Cassoneca Cassoneca 0.57 +8 Cassongue Cassongue 0.57 +9 Carcassonne Carcassonne 0.55 +10 Cassoday Cassoday 0.53 \ No newline at end of file diff --git a/src/regression-test/output/carcassonne-prefix.txt b/src/regression-test/output/carcassonne-prefix.txt new file mode 100644 index 0000000..e591958 --- /dev/null +++ b/src/regression-test/output/carcassonne-prefix.txt @@ -0,0 +1,22 @@ +{ + "string": "carcasso", + "topN": 10, + "minQuality": 0.3 +} + +{ + "queryDuration": 9 +} + +Rank Entity Matched String Quality + +1 Carcasse Carcasse 0.74 +2 Carcassonne Carcassonne 0.63 +3 Carasso Carasso 0.63 +4 Carcas Carcas 0.63 +5 Carcasí Carcasí 0.63 +6 Carcasi Carcasi 0.63 +7 Carcaboso Carcaboso 0.57 +8 Casaracra Casaracra 0.57 +9 Cassou Cassou 0.53 +10 Caracase Caracase 0.53 \ No newline at end of file diff --git a/src/regression-test/output/carcassonne-suffix.txt b/src/regression-test/output/carcassonne-suffix.txt new file mode 100644 index 0000000..6b22d4a --- /dev/null +++ b/src/regression-test/output/carcassonne-suffix.txt @@ -0,0 +1,22 @@ +{ + "string": "sonne", + "topN": 10, + "minQuality": 0.3 +} + +{ + "queryDuration": 2 +} + +Rank Entity Matched String Quality + +1 Sonnen Sonnen 0.81 +2 Sonene Sonene 0.68 +3 Sonna Sonna 0.67 +4 Sonna Sonna 0.67 +5 Sone Sone 0.63 +6 Sonon Sonon 0.63 +7 Sone Sone 0.63 +8 Sone Sone 0.63 +9 Sonnega Sonnega 0.59 +10 Sonnar Sonnar 0.54 \ No newline at end of file diff --git a/src/regression-test/output/kuwait-city-prefix.txt b/src/regression-test/output/kuwait-city-prefix.txt new file mode 100644 index 0000000..5a11bc0 --- /dev/null +++ b/src/regression-test/output/kuwait-city-prefix.txt @@ -0,0 +1,22 @@ +{ + "string": "مدينة الك", + "topN": 10, + "minQuality": 0.3 +} + +{ + "queryDuration": 2 +} + +Rank Entity Matched String Quality + +⁦1 ⁩⁧ مدينة الرد⁩⁧ مدينة الرد⁩⁦0.69 ⁩ +⁦2 ⁩⁧ مدينة الحق⁩⁧ مدينة الحق⁩⁦0.69 ⁩ +⁦3 ⁩⁧ مدينة الكويت⁩⁧ مدينة الكويت⁩⁦0.66 ⁩ +⁦4 ⁩⁧ مدينة الشرق⁩⁧ مدينة الشرق⁩⁦0.63 ⁩ +⁦5 ⁩⁧ مدينة الشعب⁩⁧ مدينة الشعب⁩⁦0.63 ⁩ +⁦6 ⁩⁧ مدينة الصدر⁩⁧ مدينة الصدر⁩⁦0.63 ⁩ +⁦7 ⁩⁧ مدينة البعث⁩⁧ مدينة البعث⁩⁦0.63 ⁩ +⁦8 ⁩⁧ مدينة المجد⁩⁧ مدينة المجد⁩⁦0.63 ⁩ +⁦9 ⁩⁧ مدينة العرب⁩⁧ مدينة العرب⁩⁦0.63 ⁩ +⁦10 ⁩⁧ مدينة الطيب⁩⁧ مدينة الطيب⁩⁦0.63 ⁩ \ No newline at end of file diff --git a/src/regression-test/output/kuwait-city.txt b/src/regression-test/output/kuwait-city.txt new file mode 100644 index 0000000..fadcd48 --- /dev/null +++ b/src/regression-test/output/kuwait-city.txt @@ -0,0 +1,22 @@ +{ + "string": "مدينة الكويت", + "topN": 10, + "minQuality": 0.3 +} + +{ + "queryDuration": 2 +} + +Rank Entity Matched String Quality + +⁦1 ⁩⁧ مدينة الكويت⁩⁧ مدينة الكويت⁩⁦1.00 ⁩ +⁦2 ⁩⁧ مدينة الشرق⁩⁧ مدينة الشرق⁩⁦0.58 ⁩ +⁦3 ⁩⁧ مدينة الشعب⁩⁧ مدينة الشعب⁩⁦0.58 ⁩ +⁦4 ⁩⁧ مدينة الوردي⁩⁧ مدينة الوردي⁩⁦0.58 ⁩ +⁦5 ⁩⁧ مدينة الصدر⁩⁧ مدينة الصدر⁩⁦0.58 ⁩ +⁦6 ⁩⁧ مدينة البعث⁩⁧ مدينة البعث⁩⁦0.58 ⁩ +⁦7 ⁩⁧ مدينة الرد⁩⁧ مدينة الرد⁩⁦0.58 ⁩ +⁦8 ⁩⁧ مدينة العمال⁩⁧ مدينة العمال⁩⁦0.58 ⁩ +⁦9 ⁩⁧ مدينة الضباط⁩⁧ مدينة الضباط⁩⁦0.58 ⁩ +⁦10 ⁩⁧ مدينة المجد⁩⁧ مدينة المجد⁩⁦0.58 ⁩ \ No newline at end of file diff --git a/src/regression-test/output/munich-insertion.txt b/src/regression-test/output/munich-insertion.txt new file mode 100644 index 0000000..42706cb --- /dev/null +++ b/src/regression-test/output/munich-insertion.txt @@ -0,0 +1,22 @@ +{ + "string": "muniich", + "topN": 10, + "minQuality": 0.3 +} + +{ + "queryDuration": 2 +} + +Rank Entity Matched String Quality + +1 Munich Munich 0.71 +2 Munini Munini 0.59 +3 Munichis Munichis 0.53 +4 New Munich New Munich 0.52 +5 Mutuini Mutuini 0.47 +6 Muniene Muniene 0.47 +7 Mumuni Mumuni 0.47 +8 Muninga Muninga 0.47 +9 Munichoco Munichoco 0.47 +10 Munhini Munhini 0.47 \ No newline at end of file diff --git a/src/regression-test/output/tbilisi-deletion.txt b/src/regression-test/output/tbilisi-deletion.txt new file mode 100644 index 0000000..b7cc493 --- /dev/null +++ b/src/regression-test/output/tbilisi-deletion.txt @@ -0,0 +1,22 @@ +{ + "string": "თბიისი", + "topN": 10, + "minQuality": 0.3 +} + +{ + "queryDuration": 2 +} + +Rank Entity Matched String Quality + +1 თბილისი თბილისი 0.59 +2 თბილისი თბილისი 0.59 +3 ბიისი ბიისი 0.54 +4 ბიისი ბიისი 0.54 +5 თეჯისი თეჯისი 0.41 +6 თეჯისი თეჯისი 0.41 +7 ყვიბისი ყვიბისი 0.36 +8 ყვიბისი ყვიბისი 0.36 +9 თერგვისი თერგვისი 0.32 +10 თამარისი თამარისი 0.32 \ No newline at end of file diff --git a/src/regression-test/output/tbilisi.txt b/src/regression-test/output/tbilisi.txt new file mode 100644 index 0000000..ae9eb2d --- /dev/null +++ b/src/regression-test/output/tbilisi.txt @@ -0,0 +1,22 @@ +{ + "string": "თბილისი", + "topN": 10, + "minQuality": 0.3 +} + +{ + "queryDuration": 2 +} + +Rank Entity Matched String Quality + +1 თბილისი თბილისი 1.00 +2 თბილისი თბილისი 1.00 +3 მილისი მილისი 0.47 +4 მილისი მილისი 0.47 +5 სხვილისი სხვილისი 0.42 +6 სხვილისი სხვილისი 0.42 +7 თეჯისი თეჯისი 0.36 +8 წნელისი წნელისი 0.36 +9 ლისი ლისი 0.36 +10 ძალისი ძალისი 0.36 \ No newline at end of file diff --git a/src/regression-test/output/tokyo-prefix.txt b/src/regression-test/output/tokyo-prefix.txt new file mode 100644 index 0000000..1e16145 --- /dev/null +++ b/src/regression-test/output/tokyo-prefix.txt @@ -0,0 +1,22 @@ +{ + "string": "東京", + "topN": 10, + "minQuality": 0.3 +} + +{ + "queryDuration": 2 +} + +Rank Entity Matched String Quality + +1 東京都 東京都 0.47 +2 東區 東區 0.32 +3 東澳 東澳 0.32 +4 東区 東区 0.32 +5 東村 東村 0.32 +6 東豐 東豐 0.32 +7 東勢 東勢 0.32 +8 東興 東興 0.32 +9 東園 東園 0.32 +10 東片 東片 0.32 \ No newline at end of file diff --git a/src/regression-test/output/tokyo.txt b/src/regression-test/output/tokyo.txt new file mode 100644 index 0000000..6f7757b --- /dev/null +++ b/src/regression-test/output/tokyo.txt @@ -0,0 +1,13 @@ +{ + "string": "東京都", + "topN": 10, + "minQuality": 0.3 +} + +{ + "queryDuration": 2 +} + +Rank Entity Matched String Quality + +1 東京都 東京都 1.00 \ No newline at end of file diff --git "a/src/regression-test/output/t\303\274bingen-transposition.txt" "b/src/regression-test/output/t\303\274bingen-transposition.txt" new file mode 100644 index 0000000..d57bfda --- /dev/null +++ "b/src/regression-test/output/t\303\274bingen-transposition.txt" @@ -0,0 +1,22 @@ +{ + "string": "tübignen", + "topN": 10, + "minQuality": 0.3 +} + +{ + "queryDuration": 2 +} + +Rank Entity Matched String Quality + +1 Tübingen Tübingen 0.76 +2 Tüfingen Tüfingen 0.57 +3 Tusingene Tusingene 0.47 +4 Täbingen Täbingen 0.47 +5 Hübingen Hübingen 0.47 +6 Tuningen Tuningen 0.47 +7 Tumlingen Tumlingen 0.47 +8 Bübingen Bübingen 0.47 +9 Tumringen Tumringen 0.47 +10 Tuttlingen Tuttlingen 0.43 \ No newline at end of file