@@ -776,6 +776,12 @@ constructor(
776776 COMPLETION_LENGTH_WEIGHT * lengthRatio +
777777 COMPLETION_FREQUENCY_WEIGHT * frequencyScore
778778
779+ val spatialScore = calculateFullWordSpatialScore(
780+ normalizedWord,
781+ word.lowercase()
782+ )
783+ baseConfidence + = spatialScore * COMPLETION_SPATIAL_WEIGHT
784+
779785 if (userFrequency > 0 ) {
780786 val userFreqBoost = calculateFrequencyBoost(userFrequency)
781787 baseConfidence + = userFreqBoost
@@ -1253,7 +1259,67 @@ constructor(
12531259 return scoreSameLengthAlignment(input, candidate, keyPositions, twoSigmaSquared)
12541260 }
12551261
1256- return scoreDifferentLengthAlignment(input, candidate, keyPositions, twoSigmaSquared)
1262+ if (input.length > candidate.length) {
1263+ return scoreDifferentLengthAlignment(input, candidate, keyPositions, twoSigmaSquared)
1264+ }
1265+
1266+ return scorePrefixWithLookahead(input, candidate, keyPositions, twoSigmaSquared, avgKeySpacing)
1267+ }
1268+
1269+ private fun scorePrefixWithLookahead (
1270+ input : String ,
1271+ candidate : String ,
1272+ keyPositions : Map <Char , android.graphics.PointF >,
1273+ twoSigmaSquared : Double ,
1274+ avgKeySpacing : Double
1275+ ): Double {
1276+ if (input.isEmpty()) return 0.0
1277+
1278+ val lengthDiff = candidate.length - input.length
1279+ val typedScore = if (lengthDiff <= 2 ) {
1280+ maxOf(
1281+ scoreDifferentLengthAlignment(input, candidate, keyPositions, twoSigmaSquared),
1282+ scoreDirectPrefix(input, candidate, keyPositions, twoSigmaSquared)
1283+ )
1284+ } else {
1285+ scoreDirectPrefix(input, candidate, keyPositions, twoSigmaSquared)
1286+ }
1287+
1288+ val lookaheadSigma = avgKeySpacing * LOOKAHEAD_SIGMA_MULTIPLIER
1289+ val lookaheadTwoSigmaSquared = 2.0 * lookaheadSigma * lookaheadSigma
1290+
1291+ var lookaheadScoreSum = 0.0
1292+ var lookaheadWeightSum = 0.0
1293+ for (i in input.length until candidate.length) {
1294+ val positionsAhead = i - input.length
1295+ val decayIndex = minOf(positionsAhead, LOOKAHEAD_DECAY_TABLE .lastIndex)
1296+ val weight = LOOKAHEAD_BASE_WEIGHT * LOOKAHEAD_DECAY_TABLE [decayIndex]
1297+ val transitionScore = scoreCharPair(
1298+ candidate[i - 1 ],
1299+ candidate[i],
1300+ keyPositions,
1301+ lookaheadTwoSigmaSquared
1302+ )
1303+ lookaheadScoreSum + = transitionScore * weight
1304+ lookaheadWeightSum + = weight
1305+ }
1306+
1307+ val typedWeight = input.length.toDouble()
1308+ val totalWeight = typedWeight + lookaheadWeightSum
1309+ return (typedScore * typedWeight + lookaheadScoreSum) / totalWeight
1310+ }
1311+
1312+ private fun scoreDirectPrefix (
1313+ input : String ,
1314+ candidate : String ,
1315+ keyPositions : Map <Char , android.graphics.PointF >,
1316+ twoSigmaSquared : Double
1317+ ): Double {
1318+ var totalScore = 0.0
1319+ for (i in input.indices) {
1320+ totalScore + = scoreCharPair(input[i], candidate[i], keyPositions, twoSigmaSquared)
1321+ }
1322+ return if (input.isNotEmpty()) totalScore / input.length else 0.0
12571323 }
12581324
12591325 private fun scoreSameLengthAlignment (
@@ -1430,6 +1496,19 @@ constructor(
14301496 const val PROXIMITY_SIGMA_MULTIPLIER = 2.0
14311497 const val MINIMUM_LENGTH_RATIO = 0.6
14321498
1499+ const val LOOKAHEAD_BASE_WEIGHT = 0.5
1500+ const val LOOKAHEAD_DECAY = 0.7
1501+ const val LOOKAHEAD_SIGMA_MULTIPLIER = 4.0
1502+ const val COMPLETION_SPATIAL_WEIGHT = 0.15
1503+ private const val MAX_LOOKAHEAD_POSITIONS = 20
1504+
1505+ @JvmField
1506+ val LOOKAHEAD_DECAY_TABLE = DoubleArray (MAX_LOOKAHEAD_POSITIONS ) { i ->
1507+ var result = 1.0
1508+ repeat(i) { result * = LOOKAHEAD_DECAY }
1509+ result
1510+ }
1511+
14331512 const val MAX_PREFIX_COMPLETION_RESULTS = 10
14341513 const val MAX_INPUT_CODEPOINTS = 100
14351514
0 commit comments