Merge "Enable long-press digit input for tablets"
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 73ec578..52a9b9a 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -201,6 +201,7 @@
         private static final int MSG_UPDATE_SUGGESTION_STRIP = 2;
         private static final int MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 3;
         private static final int MSG_RESUME_SUGGESTIONS = 4;
+        private static final int MSG_REOPEN_DICTIONARIES = 5;
 
         private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1;
 
@@ -241,6 +242,13 @@
             case MSG_RESUME_SUGGESTIONS:
                 latinIme.restartSuggestionsOnWordTouchedByCursor();
                 break;
+            case MSG_REOPEN_DICTIONARIES:
+                latinIme.initSuggest();
+                // In theory we could call latinIme.updateSuggestionStrip() right away, but
+                // in the practice, the dictionary is not finished opening yet so we wouldn't
+                // get any suggestions. Wait one frame.
+                postUpdateSuggestionStrip();
+                break;
             }
         }
 
@@ -248,6 +256,10 @@
             sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION_STRIP), mDelayUpdateSuggestions);
         }
 
+        public void postReopenDictionaries() {
+            sendMessage(obtainMessage(MSG_REOPEN_DICTIONARIES));
+        }
+
         public void postResumeSuggestions() {
             removeMessages(MSG_RESUME_SUGGESTIONS);
             sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS), mDelayUpdateSuggestions);
@@ -261,6 +273,10 @@
             return hasMessages(MSG_UPDATE_SUGGESTION_STRIP);
         }
 
+        public boolean hasPendingReopenDictionaries() {
+            return hasMessages(MSG_REOPEN_DICTIONARIES);
+        }
+
         public void postUpdateShiftState() {
             removeMessages(MSG_UPDATE_SHIFT_STATE);
             sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), mDelayUpdateShiftState);
@@ -474,8 +490,17 @@
                 new InputAttributes(getCurrentInputEditorInfo(), isFullscreenMode());
         mSettings.loadSettings(locale, inputAttributes);
         AudioAndHapticFeedbackManager.getInstance().onSettingsChanged(mSettings.getCurrent());
-        // May need to reset the contacts dictionary depending on the user settings.
-        resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary());
+        // To load the keyboard we need to load all the settings once, but resetting the
+        // contacts dictionary should be deferred until after the new layout has been displayed
+        // to improve responsivity. In the language switching process, we post a reopenDictionaries
+        // message, then come here to read the settings for the new language before we change
+        // the layout; at this time, we need to skip resetting the contacts dictionary. It will
+        // be done later inside {@see #initSuggest()} when the reopenDictionaries message is
+        // processed.
+        if (!mHandler.hasPendingReopenDictionaries()) {
+            // May need to reset the contacts dictionary depending on the user settings.
+            resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary());
+        }
     }
 
     // Note that this method is called from a non-UI thread.
@@ -2619,18 +2644,17 @@
     // Outside LatinIME, only used by the {@link InputTestsBase} test suite.
     @UsedForTesting
     void loadKeyboard() {
-        // TODO: Why are we calling {@link #loadSettings()} and {@link #initSuggest()} in a
-        // different order than in {@link #onStartInputView}?
-        initSuggest();
+        // Since we are switching languages, the most urgent thing is to let the keyboard graphics
+        // update. LoadKeyboard does that, but we need to wait for buffer flip for it to be on
+        // the screen. Anything we do right now will delay this, so wait until the next frame
+        // before we do the rest, like reopening dictionaries and updating suggestions. So we
+        // post a message.
+        mHandler.postReopenDictionaries();
         loadSettings();
         if (mKeyboardSwitcher.getMainKeyboardView() != null) {
             // Reload keyboard because the current language has been changed.
             mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent());
         }
-        // Since we just changed languages, we should re-evaluate suggestions with whatever word
-        // we are currently composing. If we are not composing anything, we may want to display
-        // predictions or punctuation signs (which is done by the updateSuggestionStrip anyway).
-        mHandler.postUpdateSuggestionStrip();
     }
 
     // Callback called by PointerTracker through the KeyboardActionListener. This is called when a
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
index f434a12..916f483 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
@@ -449,8 +449,10 @@
             final TextView wordView = mWordViews.get(positionInStrip);
             wordView.setEnabled(true);
             wordView.setTextColor(mColorAutoCorrect);
-            final String punctuation = suggestedWords.getWord(positionInStrip);
-            wordView.setText(punctuation);
+            // {@link TextView#getTag()} is used to get the index in suggestedWords at
+            // {@link SuggestionStripView#onClick(View)}.
+            wordView.setTag(positionInStrip);
+            wordView.setText(suggestedWords.getWord(positionInStrip));
             wordView.setTextScaleX(1.0f);
             wordView.setCompoundDrawables(null, null, null, null);
             stripView.addView(wordView);
@@ -468,6 +470,8 @@
         final int wordWidth = (int)(width * mCenterSuggestionWeight);
         final CharSequence text = getEllipsizedText(word, wordWidth, wordView.getPaint());
         final float wordScaleX = wordView.getTextScaleX();
+        // {@link TextView#setTag()} is used to hold the word to be added to dictionary. The word
+        // will be extracted at {@link #getAddToDictionaryWord()}.
         wordView.setTag(word);
         wordView.setText(text);
         wordView.setTextScaleX(wordScaleX);
@@ -497,8 +501,10 @@
         hintView.setOnClickListener(listener);
     }
 
-    public CharSequence getAddToDictionaryWord() {
-        return (CharSequence)mWordToSaveView.getTag();
+    public String getAddToDictionaryWord() {
+        // String tag is set at
+        // {@link #layoutAddToDictionaryHint(String,ViewGroup,int,CharSequence,OnClickListener}.
+        return (String)mWordToSaveView.getTag();
     }
 
     public boolean isAddToDictionaryShowing(final View v) {
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
index b2b9427..2284a46 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
@@ -306,12 +306,15 @@
     @Override
     public void onClick(final View view) {
         if (mLayoutHelper.isAddToDictionaryShowing(view)) {
-            mListener.addWordToUserDictionary(mLayoutHelper.getAddToDictionaryWord().toString());
+            mListener.addWordToUserDictionary(mLayoutHelper.getAddToDictionaryWord());
             clear();
             return;
         }
 
         final Object tag = view.getTag();
+        // Integer tag is set at
+        // {@link SuggestionStripLayoutHelper#setupWordViewsTextAndColor(SuggestedWords,int)} and
+        // {@link SuggestionStripLayoutHelper#layoutPunctuationSuggestions(SuggestedWords,ViewGroup}
         if (!(tag instanceof Integer)) {
             return;
         }
diff --git a/native/jni/Android.mk b/native/jni/Android.mk
index fb60139..d5df6b6 100644
--- a/native/jni/Android.mk
+++ b/native/jni/Android.mk
@@ -53,10 +53,10 @@
         dic_nodes_cache.cpp) \
     $(addprefix suggest/core/dictionary/, \
         bigram_dictionary.cpp \
-        binary_dictionary_bigrams_reading_utils.cpp \
         binary_dictionary_format_utils.cpp \
         binary_dictionary_header.cpp \
         binary_dictionary_header_reading_utils.cpp \
+        binary_dictionary_terminal_attributes_reading_utils.cpp \
         bloom_filter.cpp \
         byte_array_utils.cpp \
         dictionary.cpp \
diff --git a/native/jni/src/suggest/core/dicnode/dic_node.h b/native/jni/src/suggest/core/dicnode/dic_node.h
index 46aa0d0..c700b01 100644
--- a/native/jni/src/suggest/core/dicnode/dic_node.h
+++ b/native/jni/src/suggest/core/dicnode/dic_node.h
@@ -28,13 +28,13 @@
 #if DEBUG_DICT
 #define LOGI_SHOW_ADD_COST_PROP \
         do { char charBuf[50]; \
-        INTS_TO_CHARS(getOutputWordBuf(), getDepth(), charBuf); \
+        INTS_TO_CHARS(getOutputWordBuf(), getNodeCodePointCount(), charBuf); \
         AKLOGI("%20s, \"%c\", size = %03d, total = %03d, index(0) = %02d, dist = %.4f, %s,,", \
                 __FUNCTION__, getNodeCodePoint(), inputSize, getTotalInputIndex(), \
                 getInputIndex(0), getNormalizedCompoundDistance(), charBuf); } while (0)
 #define DUMP_WORD_AND_SCORE(header) \
         do { char charBuf[50]; char prevWordCharBuf[50]; \
-        INTS_TO_CHARS(getOutputWordBuf(), getDepth(), charBuf); \
+        INTS_TO_CHARS(getOutputWordBuf(), getNodeCodePointCount(), charBuf); \
         INTS_TO_CHARS(mDicNodeState.mDicNodeStatePrevWord.mPrevWord, \
                 mDicNodeState.mDicNodeStatePrevWord.getPrevWordLength(), prevWordCharBuf); \
         AKLOGI("#%8s, %5f, %5f, %5f, %5f, %s, %s, %d,,", header, \
@@ -162,7 +162,7 @@
             const bool isTerminal, const bool hasMultipleChars, const bool hasChildren,
             const uint16_t additionalSubwordLength, const int *additionalSubword) {
         mIsUsed = true;
-        uint16_t newDepth = static_cast<uint16_t>(dicNode->getDepth() + 1);
+        uint16_t newDepth = static_cast<uint16_t>(dicNode->getNodeCodePointCount() + 1);
         mIsCachedForNextSuggestion = dicNode->mIsCachedForNextSuggestion;
         const uint16_t newLeavingDepth = static_cast<uint16_t>(
                 dicNode->mDicNodeProperties.getLeavingDepth() + additionalSubwordLength);
@@ -185,7 +185,7 @@
     }
 
     bool isRoot() const {
-        return getDepth() == 0;
+        return getNodeCodePointCount() == 0;
     }
 
     bool hasChildren() const {
@@ -193,12 +193,12 @@
     }
 
     bool isLeavingNode() const {
-        ASSERT(getDepth() <= getLeavingDepth());
-        return getDepth() == getLeavingDepth();
+        ASSERT(getNodeCodePointCount() <= getLeavingDepth());
+        return getNodeCodePointCount() == getLeavingDepth();
     }
 
     AK_FORCE_INLINE bool isFirstLetter() const {
-        return getDepth() == 1;
+        return getNodeCodePointCount() == 1;
     }
 
     bool isCached() const {
@@ -211,7 +211,7 @@
 
     // Used to expand the node in DicNodeUtils
     int getNodeTypedCodePoint() const {
-        return mDicNodeState.mDicNodeStateOutput.getCodePointAt(getDepth());
+        return mDicNodeState.mDicNodeStateOutput.getCodePointAt(getNodeCodePointCount());
     }
 
     bool isImpossibleBigramWord() const {
@@ -220,7 +220,7 @@
         }
         const int prevWordLen = mDicNodeState.mDicNodeStatePrevWord.getPrevWordLength()
                 - mDicNodeState.mDicNodeStatePrevWord.getPrevWordStart() - 1;
-        const int currentWordLen = getDepth();
+        const int currentWordLen = getNodeCodePointCount();
         return (prevWordLen == 1 && currentWordLen == 1);
     }
 
@@ -268,13 +268,13 @@
 
     AK_FORCE_INLINE bool isTerminalWordNode() const {
         const bool isTerminalNodes = mDicNodeProperties.isTerminal();
-        const int currentNodeDepth = getDepth();
+        const int currentNodeDepth = getNodeCodePointCount();
         const int terminalNodeDepth = mDicNodeProperties.getLeavingDepth();
         return isTerminalNodes && currentNodeDepth > 0 && currentNodeDepth == terminalNodeDepth;
     }
 
     bool shouldBeFilterdBySafetyNetForBigram() const {
-        const uint16_t currentDepth = getDepth();
+        const uint16_t currentDepth = getNodeCodePointCount();
         const int prevWordLen = mDicNodeState.mDicNodeStatePrevWord.getPrevWordLength()
                 - mDicNodeState.mDicNodeStatePrevWord.getPrevWordStart() - 1;
         return !(currentDepth > 0 && (currentDepth != 1 || prevWordLen != 1));
@@ -286,7 +286,7 @@
 
     bool isTotalInputSizeExceedingLimit() const {
         const int prevWordsLen = mDicNodeState.mDicNodeStatePrevWord.getPrevWordLength();
-        const int currentWordDepth = getDepth();
+        const int currentWordDepth = getNodeCodePointCount();
         // TODO: 3 can be 2? Needs to be investigated.
         // TODO: Have a const variable for 3 (or 2)
         return prevWordsLen + currentWordDepth > MAX_WORD_LENGTH - 3;
@@ -321,7 +321,7 @@
 
     void outputResult(int *dest) const {
         const uint16_t prevWordLength = mDicNodeState.mDicNodeStatePrevWord.getPrevWordLength();
-        const uint16_t currentDepth = getDepth();
+        const uint16_t currentDepth = getNodeCodePointCount();
         DicNodeUtils::appendTwoWords(mDicNodeState.mDicNodeStatePrevWord.mPrevWord,
                    prevWordLength, getOutputWordBuf(), currentDepth, dest);
         DUMP_WORD_AND_SCORE("OUTPUT");
@@ -480,13 +480,13 @@
         return mDicNodeProperties.getAttributesPos();
     }
 
-    inline uint16_t getDepth() const {
+    inline uint16_t getNodeCodePointCount() const {
         return mDicNodeProperties.getDepth();
     }
 
-    // "Length" includes spaces.
-    inline uint16_t getTotalLength() const {
-        return getDepth() + mDicNodeState.mDicNodeStatePrevWord.getPrevWordLength();
+    // Returns code point count including spaces
+    inline uint16_t getTotalNodeCodePointCount() const {
+        return getNodeCodePointCount() + mDicNodeState.mDicNodeStatePrevWord.getPrevWordLength();
     }
 
     AK_FORCE_INLINE void dump(const char *tag) const {
@@ -521,8 +521,8 @@
         } else if (diff < -MIN_DIFF) {
             return false;
         }
-        const int depth = getDepth();
-        const int depthDiff = right->getDepth() - depth;
+        const int depth = getNodeCodePointCount();
+        const int depthDiff = right->getNodeCodePointCount() - depth;
         if (depthDiff != 0) {
             return depthDiff > 0;
         }
diff --git a/native/jni/src/suggest/core/dictionary/binary_dictionary_bigrams_iterator.h b/native/jni/src/suggest/core/dictionary/binary_dictionary_bigrams_iterator.h
index 0856840..f2b48e9 100644
--- a/native/jni/src/suggest/core/dictionary/binary_dictionary_bigrams_iterator.h
+++ b/native/jni/src/suggest/core/dictionary/binary_dictionary_bigrams_iterator.h
@@ -18,8 +18,8 @@
 #define LATINIME_BINARY_DICTIONARY_BIGRAMS_ITERATOR_H
 
 #include "defines.h"
-#include "suggest/core/dictionary/binary_dictionary_bigrams_reading_utils.h"
 #include "suggest/core/dictionary/binary_dictionary_info.h"
+#include "suggest/core/dictionary/binary_dictionary_terminal_attributes_reading_utils.h"
 
 namespace latinime {
 
@@ -35,15 +35,17 @@
     }
 
     AK_FORCE_INLINE void next() {
-        mBigramFlags = BinaryDictionaryBigramsReadingUtils::getFlagsAndForwardPointer(
+        mBigramFlags = BinaryDictionaryTerminalAttributesReadingUtils::getFlagsAndForwardPointer(
                 mBinaryDictionaryInfo, &mPos);
-        mBigramPos = BinaryDictionaryBigramsReadingUtils::getBigramAddressAndForwardPointer(
-                mBinaryDictionaryInfo, mBigramFlags, &mPos);
-        mHasNext = BinaryDictionaryBigramsReadingUtils::hasNext(mBigramFlags);
+        mBigramPos =
+                BinaryDictionaryTerminalAttributesReadingUtils::getBigramAddressAndForwardPointer(
+                        mBinaryDictionaryInfo, mBigramFlags, &mPos);
+        mHasNext = BinaryDictionaryTerminalAttributesReadingUtils::hasNext(mBigramFlags);
     }
 
     AK_FORCE_INLINE int getProbability() const {
-        return BinaryDictionaryBigramsReadingUtils::getBigramProbability(mBigramFlags);
+        return BinaryDictionaryTerminalAttributesReadingUtils::getProbabilityFromFlags(
+                mBigramFlags);
     }
 
     AK_FORCE_INLINE int getBigramPos() const {
@@ -59,7 +61,7 @@
 
     const BinaryDictionaryInfo *const mBinaryDictionaryInfo;
     int mPos;
-    BinaryDictionaryBigramsReadingUtils::BigramFlags mBigramFlags;
+    BinaryDictionaryTerminalAttributesReadingUtils::BigramFlags mBigramFlags;
     int mBigramPos;
     bool mHasNext;
 };
diff --git a/native/jni/src/suggest/core/dictionary/binary_dictionary_bigrams_reading_utils.cpp b/native/jni/src/suggest/core/dictionary/binary_dictionary_bigrams_reading_utils.cpp
deleted file mode 100644
index 78a54b1..0000000
--- a/native/jni/src/suggest/core/dictionary/binary_dictionary_bigrams_reading_utils.cpp
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "suggest/core/dictionary/binary_dictionary_bigrams_reading_utils.h"
-
-#include "suggest/core/dictionary/binary_dictionary_info.h"
-#include "suggest/core/dictionary/byte_array_utils.h"
-
-namespace latinime {
-
-const BinaryDictionaryBigramsReadingUtils::BigramFlags
-        BinaryDictionaryBigramsReadingUtils::MASK_ATTRIBUTE_ADDRESS_TYPE = 0x30;
-const BinaryDictionaryBigramsReadingUtils::BigramFlags
-        BinaryDictionaryBigramsReadingUtils::FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE = 0x10;
-const BinaryDictionaryBigramsReadingUtils::BigramFlags
-        BinaryDictionaryBigramsReadingUtils::FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES = 0x20;
-const BinaryDictionaryBigramsReadingUtils::BigramFlags
-        BinaryDictionaryBigramsReadingUtils::FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES = 0x30;
-const BinaryDictionaryBigramsReadingUtils::BigramFlags
-        BinaryDictionaryBigramsReadingUtils::FLAG_ATTRIBUTE_OFFSET_NEGATIVE = 0x40;
-// Flag for presence of more attributes
-const BinaryDictionaryBigramsReadingUtils::BigramFlags
-        BinaryDictionaryBigramsReadingUtils::FLAG_ATTRIBUTE_HAS_NEXT = 0x80;
-// Mask for attribute probability, stored on 4 bits inside the flags byte.
-const BinaryDictionaryBigramsReadingUtils::BigramFlags
-        BinaryDictionaryBigramsReadingUtils::MASK_ATTRIBUTE_PROBABILITY = 0x0F;
-const int BinaryDictionaryBigramsReadingUtils::ATTRIBUTE_ADDRESS_SHIFT = 4;
-
-/* static */ int BinaryDictionaryBigramsReadingUtils::getBigramAddressAndForwardPointer(
-        const BinaryDictionaryInfo *const binaryDictionaryInfo, const BigramFlags flags,
-        int *const pos) {
-    int offset = 0;
-    const int origin = *pos;
-    switch (MASK_ATTRIBUTE_ADDRESS_TYPE & flags) {
-        case FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE:
-            offset = ByteArrayUtils::readUint8andAdvancePosition(
-                    binaryDictionaryInfo->getDictRoot(), pos);
-            break;
-        case FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES:
-            offset = ByteArrayUtils::readUint16andAdvancePosition(
-                    binaryDictionaryInfo->getDictRoot(), pos);
-            break;
-        case FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES:
-            offset = ByteArrayUtils::readUint24andAdvancePosition(
-                    binaryDictionaryInfo->getDictRoot(), pos);
-            break;
-    }
-    if (isOffsetNegative(flags)) {
-        return origin - offset;
-    } else {
-        return origin + offset;
-    }
-}
-
-} // namespace latinime
diff --git a/native/jni/src/suggest/core/dictionary/binary_dictionary_terminal_attributes_reading_utils.cpp b/native/jni/src/suggest/core/dictionary/binary_dictionary_terminal_attributes_reading_utils.cpp
new file mode 100644
index 0000000..0a7509c
--- /dev/null
+++ b/native/jni/src/suggest/core/dictionary/binary_dictionary_terminal_attributes_reading_utils.cpp
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "suggest/core/dictionary/binary_dictionary_terminal_attributes_reading_utils.h"
+
+#include "suggest/core/dictionary/binary_dictionary_info.h"
+#include "suggest/core/dictionary/byte_array_utils.h"
+
+namespace latinime {
+
+typedef BinaryDictionaryTerminalAttributesReadingUtils TaUtils;
+
+const TaUtils::TerminalAttributeFlags TaUtils::MASK_ATTRIBUTE_ADDRESS_TYPE = 0x30;
+const TaUtils::TerminalAttributeFlags TaUtils::FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE = 0x10;
+const TaUtils::TerminalAttributeFlags TaUtils::FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES = 0x20;
+const TaUtils::TerminalAttributeFlags TaUtils::FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES = 0x30;
+const TaUtils::TerminalAttributeFlags TaUtils::FLAG_ATTRIBUTE_OFFSET_NEGATIVE = 0x40;
+// Flag for presence of more attributes
+const TaUtils::TerminalAttributeFlags TaUtils::FLAG_ATTRIBUTE_HAS_NEXT = 0x80;
+// Mask for attribute probability, stored on 4 bits inside the flags byte.
+const TaUtils::TerminalAttributeFlags TaUtils::MASK_ATTRIBUTE_PROBABILITY = 0x0F;
+const int TaUtils::ATTRIBUTE_ADDRESS_SHIFT = 4;
+
+/* static */ int TaUtils::getBigramAddressAndForwardPointer(
+        const BinaryDictionaryInfo *const binaryDictionaryInfo, const TerminalAttributeFlags flags,
+        int *const pos) {
+    int offset = 0;
+    const int origin = *pos;
+    switch (MASK_ATTRIBUTE_ADDRESS_TYPE & flags) {
+        case FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE:
+            offset = ByteArrayUtils::readUint8andAdvancePosition(
+                    binaryDictionaryInfo->getDictRoot(), pos);
+            break;
+        case FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES:
+            offset = ByteArrayUtils::readUint16andAdvancePosition(
+                    binaryDictionaryInfo->getDictRoot(), pos);
+            break;
+        case FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES:
+            offset = ByteArrayUtils::readUint24andAdvancePosition(
+                    binaryDictionaryInfo->getDictRoot(), pos);
+            break;
+    }
+    if (isOffsetNegative(flags)) {
+        return origin - offset;
+    } else {
+        return origin + offset;
+    }
+}
+
+} // namespace latinime
diff --git a/native/jni/src/suggest/core/dictionary/binary_dictionary_bigrams_reading_utils.h b/native/jni/src/suggest/core/dictionary/binary_dictionary_terminal_attributes_reading_utils.h
similarity index 61%
rename from native/jni/src/suggest/core/dictionary/binary_dictionary_bigrams_reading_utils.h
rename to native/jni/src/suggest/core/dictionary/binary_dictionary_terminal_attributes_reading_utils.h
index e71f2a1..f38fd5a 100644
--- a/native/jni/src/suggest/core/dictionary/binary_dictionary_bigrams_reading_utils.h
+++ b/native/jni/src/suggest/core/dictionary/binary_dictionary_terminal_attributes_reading_utils.h
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
-#ifndef LATINIME_BINARY_DICTIONARY_BIGRAM_READING_UTILS_H
-#define LATINIME_BINARY_DICTIONARY_BIGRAM_READING_UTILS_H
+#ifndef LATINIME_BINARY_DICTIONARY_TERMINAL_ATTRIBUTES_READING_UTILS_H
+#define LATINIME_BINARY_DICTIONARY_TERMINAL_ATTRIBUTES_READING_UTILS_H
 
 #include <stdint.h>
 
@@ -25,10 +25,26 @@
 
 namespace latinime {
 
-class BinaryDictionaryBigramsReadingUtils {
+class BinaryDictionaryTerminalAttributesReadingUtils {
  public:
-    typedef uint8_t BigramFlags;
+    typedef uint8_t TerminalAttributeFlags;
+    typedef TerminalAttributeFlags BigramFlags;
 
+    static AK_FORCE_INLINE TerminalAttributeFlags getFlagsAndForwardPointer(
+            const BinaryDictionaryInfo *const binaryDictionaryInfo, int *const pos) {
+        return ByteArrayUtils::readUint8andAdvancePosition(
+                binaryDictionaryInfo->getDictRoot(), pos);
+    }
+
+    static AK_FORCE_INLINE int getProbabilityFromFlags(const TerminalAttributeFlags flags) {
+        return flags & MASK_ATTRIBUTE_PROBABILITY;
+    }
+
+    static AK_FORCE_INLINE bool hasNext(const TerminalAttributeFlags flags) {
+        return (flags & FLAG_ATTRIBUTE_HAS_NEXT) != 0;
+    }
+
+    // Bigrams reading methods
     static AK_FORCE_INLINE void skipExistingBigrams(
             const BinaryDictionaryInfo *const binaryDictionaryInfo, int *const pos) {
         BigramFlags flags = getFlagsAndForwardPointer(binaryDictionaryInfo, pos);
@@ -39,41 +55,27 @@
         *pos += attributeAddressSize(flags);
     }
 
-    static AK_FORCE_INLINE BigramFlags getFlagsAndForwardPointer(
-            const BinaryDictionaryInfo *const binaryDictionaryInfo, int *const pos) {
-        return ByteArrayUtils::readUint8andAdvancePosition(
-                binaryDictionaryInfo->getDictRoot(), pos);
-    }
+    static int getBigramAddressAndForwardPointer(
+            const BinaryDictionaryInfo *const binaryDictionaryInfo, const BigramFlags flags,
+                    int *const pos);
 
-    static AK_FORCE_INLINE int getBigramProbability(const BigramFlags flags) {
-        return flags & MASK_ATTRIBUTE_PROBABILITY;
-    }
+ private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(BinaryDictionaryTerminalAttributesReadingUtils);
 
-    static AK_FORCE_INLINE bool isOffsetNegative(const BigramFlags flags) {
+    static const TerminalAttributeFlags MASK_ATTRIBUTE_ADDRESS_TYPE;
+    static const TerminalAttributeFlags FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE;
+    static const TerminalAttributeFlags FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES;
+    static const TerminalAttributeFlags FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES;
+    static const TerminalAttributeFlags FLAG_ATTRIBUTE_OFFSET_NEGATIVE;
+    static const TerminalAttributeFlags FLAG_ATTRIBUTE_HAS_NEXT;
+    static const TerminalAttributeFlags MASK_ATTRIBUTE_PROBABILITY;
+    static const int ATTRIBUTE_ADDRESS_SHIFT;
+
+    static AK_FORCE_INLINE bool isOffsetNegative(const TerminalAttributeFlags flags) {
         return (flags & FLAG_ATTRIBUTE_OFFSET_NEGATIVE) != 0;
     }
 
-    static AK_FORCE_INLINE bool hasNext(const BigramFlags flags) {
-        return (flags & FLAG_ATTRIBUTE_HAS_NEXT) != 0;
-    }
-
-    static int getBigramAddressAndForwardPointer(
-            const BinaryDictionaryInfo *const binaryDictionaryInfo,
-            const BigramFlags flags, int *const pos);
-
- private:
-    DISALLOW_IMPLICIT_CONSTRUCTORS(BinaryDictionaryBigramsReadingUtils);
-
-    static const BigramFlags MASK_ATTRIBUTE_ADDRESS_TYPE;
-    static const BigramFlags FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE;
-    static const BigramFlags FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES;
-    static const BigramFlags FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES;
-    static const BigramFlags FLAG_ATTRIBUTE_OFFSET_NEGATIVE;
-    static const BigramFlags FLAG_ATTRIBUTE_HAS_NEXT;
-    static const BigramFlags MASK_ATTRIBUTE_PROBABILITY;
-    static const int ATTRIBUTE_ADDRESS_SHIFT;
-
-    static AK_FORCE_INLINE int attributeAddressSize(const BigramFlags flags) {
+    static AK_FORCE_INLINE int attributeAddressSize(const TerminalAttributeFlags flags) {
         return (flags & MASK_ATTRIBUTE_ADDRESS_TYPE) >> ATTRIBUTE_ADDRESS_SHIFT;
         /* Note: this is a value-dependant optimization of what may probably be
            more readably written this way:
@@ -87,4 +89,4 @@
     }
 };
 }
-#endif /* LATINIME_BINARY_DICTIONARY_BIGRAM_READING_UTILS_H */
+#endif /* LATINIME_BINARY_DICTIONARY_TERMINAL_ATTRIBUTES_READING_UTILS_H */
diff --git a/native/jni/src/suggest/core/dictionary/byte_array_utils.h b/native/jni/src/suggest/core/dictionary/byte_array_utils.h
index d3321f6..daa822f 100644
--- a/native/jni/src/suggest/core/dictionary/byte_array_utils.h
+++ b/native/jni/src/suggest/core/dictionary/byte_array_utils.h
@@ -57,6 +57,17 @@
         return value;
     }
 
+    static AK_FORCE_INLINE int readSint24andAdvancePosition(
+            const uint8_t *const buffer, int *const pos) {
+        const uint8_t value = readUint8(buffer, *pos);
+        if (value < 0x80) {
+            return readUint24andAdvancePosition(buffer, pos);
+        } else {
+            (*pos)++;
+            return -(((value & 0x7F) << 16) ^ readUint16andAdvancePosition(buffer, pos));
+        }
+    }
+
     static AK_FORCE_INLINE uint32_t readUint24andAdvancePosition(
             const uint8_t *const buffer, int *const pos) {
         const uint32_t value = readUint24(buffer, *pos);
diff --git a/native/jni/src/suggest/core/layout/proximity_info_state.h b/native/jni/src/suggest/core/layout/proximity_info_state.h
index cc6410a..dbcd544 100644
--- a/native/jni/src/suggest/core/layout/proximity_info_state.h
+++ b/native/jni/src/suggest/core/layout/proximity_info_state.h
@@ -90,20 +90,7 @@
         return false;
     }
 
-    // TODO: Promote insertion letter correction if that letter is a proximity of the previous
-    // letter like follows:
-    // // Demotion for a word with excessive character
-    // if (excessiveCount > 0) {
-    //     multiplyRate(WORDS_WITH_EXCESSIVE_CHARACTER_DEMOTION_RATE, &finalFreq);
-    //     if (!lastCharExceeded
-    //             && !proximityInfoState->existsAdjacentProximityChars(excessivePos)) {
-    //         // If an excessive character is not adjacent to the left char or the right char,
-    //         // we will demote this word.
-    //         multiplyRate(WORDS_WITH_EXCESSIVE_CHARACTER_OUT_OF_PROXIMITY_DEMOTION_RATE,
-    //                 &finalFreq);
-    //     }
-    // }
-    inline bool existsAdjacentProximityChars(const int index) const {
+    AK_FORCE_INLINE bool existsAdjacentProximityChars(const int index) const {
         if (index < 0 || index >= mSampledInputSize) return false;
         const int currentCodePoint = getPrimaryCodePointAt(index);
         const int leftIndex = index - 1;
diff --git a/native/jni/src/suggest/core/suggest.cpp b/native/jni/src/suggest/core/suggest.cpp
index a8f16c8..173a612 100644
--- a/native/jni/src/suggest/core/suggest.cpp
+++ b/native/jni/src/suggest/core/suggest.cpp
@@ -36,6 +36,7 @@
 const int Suggest::MIN_LEN_FOR_MULTI_WORD_AUTOCORRECT = 16;
 const int Suggest::MIN_CONTINUOUS_SUGGESTION_INPUT_SIZE = 2;
 const float Suggest::AUTOCORRECT_CLASSIFICATION_THRESHOLD = 0.33f;
+const int Suggest::FINAL_SCORE_PENALTY_FOR_NOT_BEST_EXACT_MATCHED_WORD = 1;
 
 /**
  * Returns a set of suggestions for the given input touch points. The commitPoint argument indicates
@@ -148,6 +149,8 @@
             &doubleLetterTerminalIndex, &doubleLetterLevel);
 
     int maxScore = S_INT_MIN;
+    int bestExactMatchedNodeTerminalIndex = -1;
+    int bestExactMatchedNodeOutputWordIndex = -1;
     // Output suggestion results here
     for (int terminalIndex = 0; terminalIndex < terminalSize && outputWordIndex < MAX_RESULTS;
             ++terminalIndex) {
@@ -186,7 +189,6 @@
         const int finalScore = SCORING->calculateFinalScore(
                 compoundDistance, traverseSession->getInputSize(),
                 isForceCommitMultiWords || (isValidWord && SCORING->doesAutoCorrectValidWord()));
-
         maxScore = max(maxScore, finalScore);
 
         if (TRAVERSAL->allowPartialCommit()) {
@@ -200,6 +202,25 @@
         if (isValidWord) {
             outputTypes[outputWordIndex] = Dictionary::KIND_CORRECTION | outputTypeFlags;
             frequencies[outputWordIndex] = finalScore;
+            if (isSafeExactMatch) {
+                // Demote exact matches that are not the highest probable node among all exact
+                // matches.
+                const bool isBestTerminal = bestExactMatchedNodeTerminalIndex < 0
+                        || terminals[bestExactMatchedNodeTerminalIndex].getProbability()
+                                < terminalDicNode->getProbability();
+                const int outputWordIndexToBeDemoted = isBestTerminal ?
+                        bestExactMatchedNodeOutputWordIndex : outputWordIndex;
+                if (outputWordIndexToBeDemoted >= 0) {
+                    frequencies[outputWordIndexToBeDemoted] -=
+                            FINAL_SCORE_PENALTY_FOR_NOT_BEST_EXACT_MATCHED_WORD;
+                }
+                if (isBestTerminal) {
+                    // Updates the best exact matched node index.
+                    bestExactMatchedNodeTerminalIndex = terminalIndex;
+                    // Updates the best exact matched output word index.
+                    bestExactMatchedNodeOutputWordIndex = outputWordIndex;
+                }
+            }
             // Populate the outputChars array with the suggested word.
             const int startIndex = outputWordIndex * MAX_WORD_LENGTH;
             terminalDicNode->outputResult(&outputCodePoints[startIndex]);
diff --git a/native/jni/src/suggest/core/suggest.h b/native/jni/src/suggest/core/suggest.h
index 875cbe4..752bde9 100644
--- a/native/jni/src/suggest/core/suggest.h
+++ b/native/jni/src/suggest/core/suggest.h
@@ -82,6 +82,8 @@
 
     // Threshold for autocorrection classifier
     static const float AUTOCORRECT_CLASSIFICATION_THRESHOLD;
+    // Final score penalty to exact match words that are not the most probable exact match.
+    static const int FINAL_SCORE_PENALTY_FOR_NOT_BEST_EXACT_MATCHED_WORD;
 
     const Traversal *const TRAVERSAL;
     const Scoring *const SCORING;
diff --git a/native/jni/src/suggest/policyimpl/typing/scoring_params.cpp b/native/jni/src/suggest/policyimpl/typing/scoring_params.cpp
index f879892..2659e4a 100644
--- a/native/jni/src/suggest/policyimpl/typing/scoring_params.cpp
+++ b/native/jni/src/suggest/policyimpl/typing/scoring_params.cpp
@@ -33,6 +33,7 @@
 const float ScoringParams::OMISSION_COST_FIRST_CHAR = 0.582f;
 const float ScoringParams::INSERTION_COST = 0.730f;
 const float ScoringParams::INSERTION_COST_SAME_CHAR = 0.586f;
+const float ScoringParams::INSERTION_COST_PROXIMITY_CHAR = 0.70f;
 const float ScoringParams::INSERTION_COST_FIRST_CHAR = 0.623f;
 const float ScoringParams::TRANSPOSITION_COST = 0.516f;
 const float ScoringParams::SPACE_SUBSTITUTION_COST = 0.319f;
diff --git a/native/jni/src/suggest/policyimpl/typing/scoring_params.h b/native/jni/src/suggest/policyimpl/typing/scoring_params.h
index 53ac999..c39c417 100644
--- a/native/jni/src/suggest/policyimpl/typing/scoring_params.h
+++ b/native/jni/src/suggest/policyimpl/typing/scoring_params.h
@@ -42,6 +42,7 @@
     static const float OMISSION_COST_FIRST_CHAR;
     static const float INSERTION_COST;
     static const float INSERTION_COST_SAME_CHAR;
+    static const float INSERTION_COST_PROXIMITY_CHAR;
     static const float INSERTION_COST_FIRST_CHAR;
     static const float TRANSPOSITION_COST;
     static const float SPACE_SUBSTITUTION_COST;
diff --git a/native/jni/src/suggest/policyimpl/typing/typing_traversal.h b/native/jni/src/suggest/policyimpl/typing/typing_traversal.h
index e21b318..5ae396e 100644
--- a/native/jni/src/suggest/policyimpl/typing/typing_traversal.h
+++ b/native/jni/src/suggest/policyimpl/typing/typing_traversal.h
@@ -147,7 +147,7 @@
     AK_FORCE_INLINE bool sameAsTyped(
             const DicTraverseSession *const traverseSession, const DicNode *const dicNode) const {
         return traverseSession->getProximityInfoState(0)->sameAsTyped(
-                dicNode->getOutputWordBuf(), dicNode->getDepth());
+                dicNode->getOutputWordBuf(), dicNode->getNodeCodePointCount());
     }
 
     AK_FORCE_INLINE int getMaxCacheSize() const {
@@ -171,7 +171,7 @@
             return false;
         }
         const int c = dicNode->getOutputWordBuf()[0];
-        const bool shortCappedWord = dicNode->getDepth()
+        const bool shortCappedWord = dicNode->getNodeCodePointCount()
                 < ScoringParams::THRESHOLD_SHORT_WORD_LENGTH && CharUtils::isAsciiUpper(c);
         return !shortCappedWord
                 || probability >= ScoringParams::THRESHOLD_NEXT_WORD_PROBABILITY_FOR_CAPPED;
diff --git a/native/jni/src/suggest/policyimpl/typing/typing_weighting.h b/native/jni/src/suggest/policyimpl/typing/typing_weighting.h
index a1c9918..e098f35 100644
--- a/native/jni/src/suggest/policyimpl/typing/typing_weighting.h
+++ b/native/jni/src/suggest/policyimpl/typing/typing_weighting.h
@@ -55,7 +55,7 @@
         const bool isZeroCostOmission = parentDicNode->isZeroCostOmission();
         const bool sameCodePoint = dicNode->isSameNodeCodePoint(parentDicNode);
         // If the traversal omitted the first letter then the dicNode should now be on the second.
-        const bool isFirstLetterOmission = dicNode->getDepth() == 2;
+        const bool isFirstLetterOmission = dicNode->getNodeCodePointCount() == 2;
         float cost = 0.0f;
         if (isZeroCostOmission) {
             cost = 0.0f;
@@ -83,7 +83,7 @@
         const bool isProximity = isProximityDicNode(traverseSession, dicNode);
         float cost = isProximity ? (isFirstChar ? ScoringParams::FIRST_PROXIMITY_COST
                 : ScoringParams::PROXIMITY_COST) : 0.0f;
-        if (dicNode->getDepth() == 2) {
+        if (dicNode->getNodeCodePointCount() == 2) {
             // At the second character of the current word, we check if the first char is uppercase
             // and the word is a second or later word of a multiple word suggestion. We demote it
             // if so.
@@ -122,19 +122,25 @@
 
     float getInsertionCost(const DicTraverseSession *const traverseSession,
             const DicNode *const parentDicNode, const DicNode *const dicNode) const {
-        const int16_t parentPointIndex = parentDicNode->getInputIndex(0);
-        const int prevCodePoint =
-                traverseSession->getProximityInfoState(0)->getPrimaryCodePointAt(parentPointIndex);
-
+        const int16_t insertedPointIndex = parentDicNode->getInputIndex(0);
+        const int prevCodePoint = traverseSession->getProximityInfoState(0)->getPrimaryCodePointAt(
+                insertedPointIndex);
         const int currentCodePoint = dicNode->getNodeCodePoint();
         const bool sameCodePoint = prevCodePoint == currentCodePoint;
+        const bool existsAdjacentProximityChars = traverseSession->getProximityInfoState(0)
+                ->existsAdjacentProximityChars(insertedPointIndex);
         const float dist = traverseSession->getProximityInfoState(0)->getPointToKeyLength(
-                parentPointIndex + 1, currentCodePoint);
+                insertedPointIndex + 1, dicNode->getNodeCodePoint());
         const float weightedDistance = dist * ScoringParams::DISTANCE_WEIGHT_LENGTH;
-        const bool singleChar = dicNode->getDepth() == 1;
-        const float cost = (singleChar ? ScoringParams::INSERTION_COST_FIRST_CHAR : 0.0f)
-                + (sameCodePoint ? ScoringParams::INSERTION_COST_SAME_CHAR
-                        : ScoringParams::INSERTION_COST);
+        const bool singleChar = dicNode->getNodeCodePointCount() == 1;
+        float cost = (singleChar ? ScoringParams::INSERTION_COST_FIRST_CHAR : 0.0f);
+        if (sameCodePoint) {
+            cost += ScoringParams::INSERTION_COST_SAME_CHAR;
+        } else if (existsAdjacentProximityChars) {
+            cost += ScoringParams::INSERTION_COST_PROXIMITY_CHAR;
+        } else {
+            cost += ScoringParams::INSERTION_COST;
+        }
         return cost + weightedDistance;
     }
 
@@ -163,6 +169,9 @@
 
     float getTerminalLanguageCost(const DicTraverseSession *const traverseSession,
             const DicNode *const dicNode, const float dicNodeLanguageImprobability) const {
+        // We promote exact matches here to prevent them from being pruned. The final score of
+        // exact match nodes might be demoted later in Suggest::outputSuggestions if there are
+        // multiple exact matches.
         const float languageImprobability = (dicNode->isExactMatch()) ?
                 0.0f : dicNodeLanguageImprobability;
         return languageImprobability * ScoringParams::DISTANCE_WEIGHT_LANGUAGE;