Merge "Add KeyboardState.SwitchActions.setAlphabetShiftLockShifted"
diff --git a/java/src/com/android/inputmethod/latin/LastComposedWord.java b/java/src/com/android/inputmethod/latin/LastComposedWord.java
new file mode 100644
index 0000000..767c3a7
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/LastComposedWord.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.inputmethod.latin;
+
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+
+/**
+ * This class encapsulates data about a word previously composed, but that has been
+ * committed already. This is used for resuming suggestion, and cancel auto-correction.
+ */
+public class LastComposedWord {
+    // COMMIT_TYPE_USER_TYPED_WORD is used when the word committed is the exact typed word, with
+    // no hinting from the IME. It happens when some external event happens (rotating the device,
+    // for example) or when auto-correction is off by settings or editor attributes.
+    public static final int COMMIT_TYPE_USER_TYPED_WORD = 0;
+    // COMMIT_TYPE_MANUAL_PICK is used when the user pressed a field in the suggestion strip.
+    public static final int COMMIT_TYPE_MANUAL_PICK = 1;
+    // COMMIT_TYPE_DECIDED_WORD is used when the IME commits the word it decided was best
+    // for the current user input. It may be different from what the user typed (true auto-correct)
+    // or it may be exactly what the user typed if it's in the dictionary or the IME does not have
+    // enough confidence in any suggestion to auto-correct (auto-correct to typed word).
+    public static final int COMMIT_TYPE_DECIDED_WORD = 2;
+    // COMMIT_TYPE_CANCEL_AUTO_CORRECT is used upon committing back the old word upon cancelling
+    // an auto-correction.
+    public static final int COMMIT_TYPE_CANCEL_AUTO_CORRECT = 3;
+
+    public final int mType;
+    public final ArrayList<int[]> mCodes;
+    public final int[] mXCoordinates;
+    public final int[] mYCoordinates;
+    public final String mTypedWord;
+    public final String mAutoCorrection;
+
+    private boolean mActive;
+
+    public static final LastComposedWord NOT_A_COMPOSED_WORD =
+            new LastComposedWord(COMMIT_TYPE_USER_TYPED_WORD, null, null, null, "", "");
+
+    public LastComposedWord(final int type, final ArrayList<int[]> codes, final int[] xCoordinates,
+            final int[] yCoordinates, final String typedWord, final String autoCorrection) {
+        mType = type;
+        mCodes = codes;
+        mXCoordinates = xCoordinates;
+        mYCoordinates = yCoordinates;
+        mTypedWord = typedWord;
+        mAutoCorrection = autoCorrection;
+        mActive = true;
+    }
+
+    public void deactivate() {
+        mActive = false;
+    }
+
+    public boolean canCancelAutoCorrect() {
+        return mActive && !TextUtils.isEmpty(mAutoCorrection)
+                && !TextUtils.equals(mTypedWord, mAutoCorrection);
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index f2fa788..2e7e826 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -199,6 +199,7 @@
     private UserUnigramDictionary mUserUnigramDictionary;
     private boolean mIsUserDictionaryAvailable;
 
+    private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
     private WordComposer mWordComposer = new WordComposer();
 
     private int mCorrectionMode;
@@ -769,7 +770,7 @@
 
         inputView.closing();
         mEnteredText = null;
-        mWordComposer.reset();
+        resetComposingState(true /* alsoResetLastComposedWord */);
         mDeleteCount = 0;
         mSpaceState = SPACE_STATE_NONE;
 
@@ -881,7 +882,7 @@
             if (((mWordComposer.isComposingWord())
                     || mVoiceProxy.isVoiceInputHighlighted())
                     && (selectionChanged || candidatesCleared)) {
-                mWordComposer.reset();
+                resetComposingState(true /* alsoResetLastComposedWord */);
                 updateSuggestions();
                 final InputConnection ic = getCurrentInputConnection();
                 if (ic != null) {
@@ -890,7 +891,9 @@
                 mComposingStateManager.onFinishComposingText();
                 mVoiceProxy.setVoiceInputHighlighted(false);
             } else if (!mWordComposer.isComposingWord()) {
-                mWordComposer.reset();
+                // TODO: is the following reset still needed, given that we are not composing
+                // a word?
+                resetComposingState(true /* alsoResetLastComposedWord */);
                 updateSuggestions();
             }
         }
@@ -974,7 +977,9 @@
                     .setHasMinimalSuggestion(false);
             // When in fullscreen mode, show completions generated by the application
             setSuggestions(builder.build());
-            mWordComposer.deleteAutoCorrection();
+            // TODO: is this the right thing to do? What should we auto-correct to in
+            // this case? This says to keep whatever the user typed.
+            mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
             setSuggestionStripShown(true);
         }
     }
@@ -1093,10 +1098,16 @@
         return super.onKeyUp(keyCode, event);
     }
 
+    private void resetComposingState(final boolean alsoResetLastComposedWord) {
+        mWordComposer.reset();
+        if (alsoResetLastComposedWord)
+            mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
+    }
+
     public void commitTyped(final InputConnection ic) {
         if (!mWordComposer.isComposingWord()) return;
         final CharSequence typedWord = mWordComposer.getTypedWord();
-        mWordComposer.onCommitWord(WordComposer.COMMIT_TYPE_USER_TYPED_WORD);
+        mLastComposedWord = mWordComposer.commitWord(LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD);
         if (typedWord.length() > 0) {
             if (ic != null) {
                 ic.commitText(typedWord, 1);
@@ -1263,6 +1274,7 @@
             mHandler.cancelDoubleSpacesTimer();
         }
 
+        boolean didAutoCorrect = false;
         switch (primaryCode) {
         case Keyboard.CODE_DELETE:
             mSpaceState = SPACE_STATE_NONE;
@@ -1299,7 +1311,7 @@
         default:
             mSpaceState = SPACE_STATE_NONE;
             if (mSettingsValues.isWordSeparator(primaryCode)) {
-                handleSeparator(primaryCode, x, y, spaceState);
+                didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState);
             } else {
                 handleCharacter(primaryCode, keyCodes, x, y, spaceState);
             }
@@ -1308,6 +1320,8 @@
         }
         switcher.onCodeInput(primaryCode);
         // Reset after any single keystroke
+        if (!didAutoCorrect)
+            mLastComposedWord.deactivate();
         mEnteredText = null;
     }
 
@@ -1325,7 +1339,7 @@
         mKeyboardSwitcher.onCodeInput(Keyboard.CODE_OUTPUT_TEXT);
         mSpaceState = SPACE_STATE_NONE;
         mEnteredText = text;
-        mWordComposer.reset();
+        resetComposingState(true /* alsoResetLastComposedWord */);
     }
 
     @Override
@@ -1383,7 +1397,7 @@
             // resuming here. The behavior needs to be different according to text field types,
             // and it would be much clearer to test for them explicitly here rather than
             // relying on implicit values like "whether the suggestion strip is displayed".
-            if (mWordComposer.didAutoCorrectToAnotherWord()) {
+            if (mLastComposedWord.canCancelAutoCorrect()) {
                 Utils.Stats.onAutoCorrectionCancellation();
                 cancelAutoCorrect(ic);
                 return;
@@ -1495,7 +1509,11 @@
                 // separator and it should be treated as a normal character, except in the first
                 // position where it should not start composing a word.
                 isComposingWord = (Keyboard.CODE_SINGLE_QUOTE != code);
-                mWordComposer.reset();
+                // Here we don't need to reset the last composed word. It will be reset
+                // when we commit this one, if we ever do; if on the other hand we backspace
+                // it entirely and resume suggestions on the previous word, we'd like to still
+                // have touch coordinates for it.
+                resetComposingState(false /* alsoResetLastComposedWord */);
                 clearSuggestions();
                 mComposingStateManager.onFinishComposingText();
             }
@@ -1547,7 +1565,8 @@
         }
     }
 
-    private void handleSeparator(final int primaryCode, final int x, final int y,
+    // Returns true if we did an autocorrection, false otherwise.
+    private boolean handleSeparator(final int primaryCode, final int x, final int y,
             final int spaceState) {
         mVoiceProxy.handleSeparator();
         mComposingStateManager.onFinishComposingText();
@@ -1558,6 +1577,7 @@
             mHandler.postUpdateSuggestions();
         }
 
+        boolean didAutoCorrect = false;
         // Handle separator
         final InputConnection ic = getCurrentInputConnection();
         if (ic != null) {
@@ -1572,6 +1592,7 @@
                     && !mInputAttributes.mInputTypeNoAutoCorrect;
             if (shouldAutoCorrect && primaryCode != Keyboard.CODE_SINGLE_QUOTE) {
                 commitCurrentAutoCorrection(primaryCode, ic);
+                didAutoCorrect = true;
             } else {
                 commitTyped(ic);
             }
@@ -1627,6 +1648,7 @@
         if (ic != null) {
             ic.endBatchEdit();
         }
+        return didAutoCorrect;
     }
 
     private CharSequence getTextWithUnderline(final CharSequence text) {
@@ -1847,7 +1869,7 @@
             }
             Utils.Stats.onAutoCorrection(typedWord, autoCorrection.toString(), separatorCodePoint);
             mExpectingUpdateSelection = true;
-            commitChosenWord(autoCorrection, WordComposer.COMMIT_TYPE_DECIDED_WORD);
+            commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD);
             // Add the word to the user unigram dictionary if it's not a known word
             addToUserUnigramAndBigramDictionaries(autoCorrection,
                     UserUnigramDictionary.FREQUENCY_FOR_TYPED);
@@ -1917,7 +1939,7 @@
         LatinImeLogger.logOnManualSuggestion(mWordComposer.getTypedWord().toString(),
                 suggestion.toString(), index, suggestions.mWords);
         mExpectingUpdateSelection = true;
-        commitChosenWord(suggestion, WordComposer.COMMIT_TYPE_MANUAL_PICK);
+        commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK);
         // Add the word to the auto dictionary if it's not a known word
         if (index == 0) {
             addToUserUnigramAndBigramDictionaries(suggestion,
@@ -1985,9 +2007,9 @@
         }
         // TODO: figure out here if this is an auto-correct or if the best word is actually
         // what user typed. Note: currently this is done much later in
-        // WordComposer#didAutoCorrectToAnotherWord by string equality of the remembered
+        // LastComposedWord#canCancelAutoCorrect by string equality of the remembered
         // strings.
-        mWordComposer.onCommitWord(commitType);
+        mLastComposedWord = mWordComposer.commitWord(commitType);
     }
 
     private static final WordComposer sEmptyWordComposer = new WordComposer();
@@ -2151,12 +2173,14 @@
 
     // "ic" must not be null
     private void cancelAutoCorrect(final InputConnection ic) {
-        mWordComposer.resumeSuggestionOnKeptWord();
-        final String originallyTypedWord = mWordComposer.getTypedWord();
-        final CharSequence autoCorrectedTo = mWordComposer.getAutoCorrectionOrNull();
+        final String originallyTypedWord = mLastComposedWord.mTypedWord;
+        final CharSequence autoCorrectedTo = mLastComposedWord.mAutoCorrection;
         final int cancelLength = autoCorrectedTo.length();
         final CharSequence separator = ic.getTextBeforeCursor(1, 0);
         if (DEBUG) {
+            if (mWordComposer.isComposingWord()) {
+                throw new RuntimeException("cancelAutoCorrect, but we are composing a word");
+            }
             final String wordBeforeCursor =
                     ic.getTextBeforeCursor(cancelLength + 1, 0).subSequence(0, cancelLength)
                     .toString();
@@ -2175,8 +2199,7 @@
         ic.commitText(originallyTypedWord, 1);
         // Re-insert the separator
         ic.commitText(separator, 1);
-        mWordComposer.deleteAutoCorrection();
-        mWordComposer.onCommitWord(WordComposer.COMMIT_TYPE_CANCEL_AUTO_CORRECT);
+        mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
         Utils.Stats.onSeparator(separator.charAt(0), WordComposer.NOT_A_COORDINATE,
                 WordComposer.NOT_A_COORDINATE);
         mHandler.cancelUpdateBigramPredictions();
@@ -2190,7 +2213,7 @@
         // Note: in the interest of code simplicity, we may want to just call
         // restartSuggestionsOnWordBeforeCursorIfAtEndOfWord instead, but retrieving
         // the old WordComposer allows to reuse the actual typed coordinates.
-        mWordComposer.resumeSuggestionOnKeptWord();
+        mWordComposer.resumeSuggestionOnLastComposedWord(mLastComposedWord);
         // We resume suggestion, and then we want to set the composing text to the content
         // of the word composer again. But since we just manually picked a word, there is
         // no composing text at the moment, so we have to delete the word before we set a
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index e95dcfd..bf132ed 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -33,23 +33,6 @@
     public static final int NOT_A_CODE = KeyDetector.NOT_A_CODE;
     public static final int NOT_A_COORDINATE = -1;
 
-    // TODO: Straighten out commit behavior so that the flags here are more understandable,
-    // and possibly adjust their names.
-    // COMMIT_TYPE_USER_TYPED_WORD is used when the word committed is the exact typed word, with
-    // no hinting from the IME. It happens when some external event happens (rotating the device,
-    // for example) or when auto-correction is off by settings or editor attributes.
-    public static final int COMMIT_TYPE_USER_TYPED_WORD = 0;
-    // COMMIT_TYPE_MANUAL_PICK is used when the user pressed a field in the suggestion strip.
-    public static final int COMMIT_TYPE_MANUAL_PICK = 1;
-    // COMMIT_TYPE_DECIDED_WORD is used when the IME commits the word it decided was best
-    // for the current user input. It may be different from what the user typed (true auto-correct)
-    // or it may be exactly what the user typed if it's in the dictionary or the IME does not have
-    // enough confidence in any suggestion to auto-correct (auto-correct to typed word).
-    public static final int COMMIT_TYPE_DECIDED_WORD = 2;
-    // COMMIT_TYPE_CANCEL_AUTO_CORRECT is used upon committing back the old word upon cancelling
-    // an auto-correction.
-    public static final int COMMIT_TYPE_CANCEL_AUTO_CORRECT = 3;
-
     // Storage for all the info about the current input.
     private static class CharacterStore {
         /**
@@ -84,8 +67,6 @@
 
     // The currently typing word. May not be null.
     private CharacterStore mCurrentWord;
-    // The information being kept for resuming suggestion. May be null if wiped.
-    private CharacterStore mCommittedWordSavedForSuggestionResuming;
 
     private int mCapsCount;
 
@@ -100,7 +81,6 @@
 
     public WordComposer() {
         mCurrentWord = new CharacterStore();
-        mCommittedWordSavedForSuggestionResuming = null;
         mTrailingSingleQuotesCount = 0;
     }
 
@@ -110,7 +90,6 @@
 
     public void init(WordComposer source) {
         mCurrentWord = new CharacterStore(source.mCurrentWord);
-        mCommittedWordSavedForSuggestionResuming = source.mCommittedWordSavedForSuggestionResuming;
         mCapsCount = source.mCapsCount;
         mIsFirstCharCapitalized = source.mIsFirstCharCapitalized;
         mAutoCapitalized = source.mAutoCapitalized;
@@ -122,7 +101,6 @@
      */
     public void reset() {
         mCurrentWord.reset();
-        mCommittedWordSavedForSuggestionResuming = null;
         mCapsCount = 0;
         mIsFirstCharCapitalized = false;
         mTrailingSingleQuotesCount = 0;
@@ -218,7 +196,6 @@
             int codePoint = word.charAt(i);
             addKeyInfo(codePoint, keyboard, keyDetector);
         }
-        mCommittedWordSavedForSuggestionResuming = null;
     }
 
     /**
@@ -333,22 +310,14 @@
     }
 
     /**
-     * Remove any auto-correction that may have been set.
-     */
-    public void deleteAutoCorrection() {
-        mCurrentWord.mAutoCorrection = null;
-    }
-
-    /**
      * @return the auto-correction for this word, or null if none.
      */
     public CharSequence getAutoCorrectionOrNull() {
         return mCurrentWord.mAutoCorrection;
     }
 
-    // `type' should be one of the COMMIT_TYPE_* constants above.
-    public void onCommitWord(final int type) {
-        mCommittedWordSavedForSuggestionResuming = mCurrentWord;
+    // `type' should be one of the LastComposedWord.COMMIT_TYPE_* constants above.
+    public LastComposedWord commitWord(final int type) {
         // Note: currently, we come here whenever we commit a word. If it's any *other* kind than
         // DECIDED_WORD, we should reset mAutoCorrection so that we don't attempt to cancel later.
         // If it's a DECIDED_WORD, it may be an actual auto-correction by the IME, or what the user
@@ -356,30 +325,26 @@
         // Ideally we would also null it when it was a DECIDED_WORD that was not an auto-correct.
         // As it happens these two cases should behave differently, because the former can be
         // canceled while the latter can't. Currently, we figure this out in
-        // #didAutoCorrectToAnotherWord with #equals(). It would be marginally cleaner to do it
-        // here, but it would be slower (since we would #equals() for each commit, instead of
-        // only on cancel), and ultimately we want to figure it out even earlier anyway.
-        if (type != COMMIT_TYPE_DECIDED_WORD) {
-            // Only ever revert an auto-correct.
-            mCommittedWordSavedForSuggestionResuming.mAutoCorrection = null;
-        }
+        // LastComposedWord#didAutoCorrectToAnotherWord with #equals(). It would be marginally
+        // cleaner to do it here, but it would be slower (since we would #equals() for each commit,
+        // instead of only on cancel), and ultimately we want to figure it out even earlier anyway.
+        final LastComposedWord lastComposedWord = new LastComposedWord(type, mCurrentWord.mCodes,
+                mCurrentWord.mXCoordinates, mCurrentWord.mYCoordinates,
+                mCurrentWord.mTypedWord.toString(),
+                (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD)
+                        || (null == mCurrentWord.mAutoCorrection)
+                                ? null : mCurrentWord.mAutoCorrection.toString());
         // TODO: improve performance by swapping buffers instead of creating a new object.
         mCurrentWord = new CharacterStore();
+        return lastComposedWord;
     }
 
-    public boolean hasWordKeptForSuggestionResuming() {
-        return null != mCommittedWordSavedForSuggestionResuming;
-    }
-
-    public void resumeSuggestionOnKeptWord() {
-        mCurrentWord = mCommittedWordSavedForSuggestionResuming;
-        mCommittedWordSavedForSuggestionResuming = null;
-    }
-
-    public boolean didAutoCorrectToAnotherWord() {
-        return null != mCommittedWordSavedForSuggestionResuming
-                && !TextUtils.isEmpty(mCommittedWordSavedForSuggestionResuming.mAutoCorrection)
-                && !TextUtils.equals(mCommittedWordSavedForSuggestionResuming.mTypedWord,
-                        mCommittedWordSavedForSuggestionResuming.mAutoCorrection);
+    public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord) {
+        mCurrentWord.mCodes = lastComposedWord.mCodes;
+        mCurrentWord.mXCoordinates = lastComposedWord.mXCoordinates;
+        mCurrentWord.mYCoordinates = lastComposedWord.mYCoordinates;
+        mCurrentWord.mTypedWord.setLength(0);
+        mCurrentWord.mTypedWord.append(lastComposedWord.mTypedWord);
+        mCurrentWord.mAutoCorrection = lastComposedWord.mAutoCorrection;
     }
 }
diff --git a/native/src/defines.h b/native/src/defines.h
index 9c2d087..7e171ac 100644
--- a/native/src/defines.h
+++ b/native/src/defines.h
@@ -217,6 +217,7 @@
 #define SUB_QUEUE_MAX_WORDS 1
 #define SUB_QUEUE_MAX_COUNT 10
 #define SUB_QUEUE_MIN_WORD_LENGTH 4
+#define SUB_QUEUE_MAX_WORD_INDEX 2
 
 #define TWO_WORDS_CORRECTION_WITH_OTHER_ERROR_THRESHOLD 0.39
 #define START_TWO_WORDS_CORRECTION_THRESHOLD 0.22
diff --git a/native/src/unigram_dictionary.cpp b/native/src/unigram_dictionary.cpp
index 6a89737..fd6f14a 100644
--- a/native/src/unigram_dictionary.cpp
+++ b/native/src/unigram_dictionary.cpp
@@ -260,7 +260,7 @@
     if (DEBUG_DICT) {
         queuePool->dumpSubQueue1TopSuggestions();
         for (int i = 0; i < SUB_QUEUE_MAX_COUNT; ++i) {
-            WordsPriorityQueue* queue = queuePool->getSubQueue1(i);
+            WordsPriorityQueue* queue = queuePool->getSubQueue(FIRST_WORD_INDEX, i);
             if (queue->size() > 0) {
                 WordsPriorityQueue::SuggestedWord* sw = queue->top();
                 const int score = sw->mScore;
@@ -395,11 +395,8 @@
     // or more length.
     if (inputIndex >= SUB_QUEUE_MIN_WORD_LENGTH && addToSubQueue) {
         WordsPriorityQueue *subQueue;
-        if (currentWordIndex == 1) {
-            subQueue = queuePool->getSubQueue1(inputIndex);
-        } else if (currentWordIndex == 2) {
-            subQueue = queuePool->getSubQueue2(inputIndex);
-        } else {
+        subQueue = queuePool->getSubQueue(currentWordIndex, inputIndex);
+        if (!subQueue) {
             return;
         }
         const int finalFreq = correction->getFinalFreqForSubQueue(freq, &wordPointer, &wordLength,
@@ -408,6 +405,78 @@
     }
 }
 
+int UnigramDictionary::getSubStringSuggestion(
+        ProximityInfo *proximityInfo, const int *xcoordinates, const int *ycoordinates,
+        const int *codes, const bool useFullEditDistance, Correction *correction,
+        WordsPriorityQueuePool* queuePool, const int inputLength,
+        const bool hasAutoCorrectionCandidate, const int currentWordIndex,
+        const int inputWordStartPos, const int inputWordLength,
+        const int outputWordStartPos, unsigned short* outputWord, int *outputWordLength) {
+    unsigned short* tempOutputWord = 0;
+    int tempOutputWordLength = 0;
+    int freq = getMostFrequentWordLike(
+            inputWordStartPos, inputWordLength, proximityInfo, mWord);
+    if (freq > 0) {
+        tempOutputWordLength = inputWordLength;
+        tempOutputWord = mWord;
+    } else if (!hasAutoCorrectionCandidate) {
+        if (inputWordStartPos > 0) {
+            const int offset = inputWordStartPos;
+            initSuggestions(proximityInfo, &xcoordinates[offset], &ycoordinates[offset],
+                    codes + offset * MAX_PROXIMITY_CHARS, inputWordLength, correction);
+            queuePool->clearSubQueue(currentWordIndex);
+            getSuggestionCandidates(useFullEditDistance, inputWordLength, correction,
+                    queuePool, false, MAX_ERRORS_FOR_TWO_WORDS, currentWordIndex);
+            if (DEBUG_DICT) {
+                if (currentWordIndex <= SUB_QUEUE_MAX_WORD_INDEX) {
+                    AKLOGI("Dump word candidates(%d) %d", currentWordIndex, inputWordLength);
+                    for (int i = 0; i < SUB_QUEUE_MAX_COUNT; ++i) {
+                        queuePool->getSubQueue(currentWordIndex, i)->dumpTopWord();
+                    }
+                }
+            }
+        }
+        WordsPriorityQueue* queue = queuePool->getSubQueue(currentWordIndex, inputWordLength);
+        if (!queue || queue->size() < 1) {
+            return 0;
+        }
+        int score = 0;
+        const double ns = queue->getHighestNormalizedScore(
+                proximityInfo->getPrimaryInputWord(), inputWordLength,
+                &tempOutputWord, &score, &tempOutputWordLength);
+        if (DEBUG_DICT) {
+            AKLOGI("NS(%d) = %f, Score = %d", currentWordIndex, ns, score);
+        }
+        // Two words correction won't be done if the score of the first word doesn't exceed the
+        // threshold.
+        if (ns < TWO_WORDS_CORRECTION_WITH_OTHER_ERROR_THRESHOLD
+                || tempOutputWordLength < SUB_QUEUE_MIN_WORD_LENGTH) {
+            return 0;
+        }
+        freq = score >> (tempOutputWordLength
+                + TWO_WORDS_PLUS_OTHER_ERROR_CORRECTION_DEMOTION_DIVIDER);
+    }
+    if (DEBUG_DICT) {
+        AKLOGI("Freq(%d): %d", currentWordIndex, freq);
+    }
+    if (freq <= 0 || tempOutputWordLength <= 0
+            || MAX_WORD_LENGTH <= (outputWordStartPos + tempOutputWordLength)) {
+        return 0;
+    }
+    for (int i = 0; i < tempOutputWordLength; ++i) {
+        outputWord[outputWordStartPos + i] = tempOutputWord[i];
+    }
+    if ((inputWordStartPos + inputWordLength) < inputLength) {
+        if (outputWordStartPos + tempOutputWordLength >= MAX_WORD_LENGTH) {
+            return 0;
+        }
+        outputWord[outputWordStartPos + tempOutputWordLength] = SPACE;
+        ++tempOutputWordLength;
+    }
+    *outputWordLength = outputWordStartPos + tempOutputWordLength;
+    return freq;
+}
+
 void UnigramDictionary::getSplitTwoWordsSuggestions(ProximityInfo *proximityInfo,
         const int *xcoordinates, const int *ycoordinates, const int *codes,
         const bool useFullEditDistance, const int inputLength, const int missingSpacePos,
@@ -425,124 +494,36 @@
 
     initSuggestions(proximityInfo, xcoordinates, ycoordinates, codes,
             inputLength, correction);
-    WordsPriorityQueue *masterQueue = queuePool->getMasterQueue();
-    const bool isSpaceProximity = spaceProximityPos >= 0;
-
-    // First word
-    const int firstInputWordStartPos = 0;
-    const int firstInputWordLength = isSpaceProximity ? spaceProximityPos : missingSpacePos;
-    int firstFreq = getMostFrequentWordLike(
-            firstInputWordStartPos, firstInputWordLength, proximityInfo, mWord);
-    unsigned short* firstOutputWord = 0;
-    int firstOutputWordLength = 0;
-    if (firstFreq > 0) {
-        firstOutputWordLength = firstInputWordLength;
-        firstOutputWord = mWord;
-    } else if (!hasAutoCorrectionCandidate) {
-        WordsPriorityQueue* firstWordQueue = queuePool->getSubQueue1(firstInputWordLength);
-        if (!firstWordQueue || firstWordQueue->size() < 1) {
-            return;
-        }
-        int score = 0;
-        const double ns = firstWordQueue->getHighestNormalizedScore(
-                proximityInfo->getPrimaryInputWord(), firstInputWordLength,
-                &firstOutputWord, &score, &firstOutputWordLength);
-        if (DEBUG_DICT) {
-            AKLOGI("NS1 = %f, Score = %d", ns, score);
-        }
-        // Two words correction won't be done if the score of the first word doesn't exceed the
-        // threshold.
-        if (ns < TWO_WORDS_CORRECTION_WITH_OTHER_ERROR_THRESHOLD
-                || firstOutputWordLength < SUB_QUEUE_MIN_WORD_LENGTH) {
-            return;
-        }
-        firstFreq = score >> (firstOutputWordLength
-                + TWO_WORDS_PLUS_OTHER_ERROR_CORRECTION_DEMOTION_DIVIDER);
-    }
-
-    if (DEBUG_DICT) {
-        AKLOGI("First freq: %d", firstFreq);
-    }
-
-    if (firstFreq <= 0 || firstOutputWordLength <= 0 || MAX_WORD_LENGTH <= firstOutputWordLength) {
-        return;
-    }
 
     // Allocating fixed length array on stack
     unsigned short outputWord[MAX_WORD_LENGTH];
     int outputWordLength = 0;
 
-    for (int i = 0; i < firstOutputWordLength; ++i) {
-        outputWord[i] = firstOutputWord[i];
-    }
+    WordsPriorityQueue *masterQueue = queuePool->getMasterQueue();
+    const bool isSpaceProximity = spaceProximityPos >= 0;
 
-    outputWord[firstOutputWordLength] = SPACE;
-    outputWordLength = firstOutputWordLength + 1;
-
-    // Second word
-    const int secondInputWordLength = isSpaceProximity
-            ? (inputLength - spaceProximityPos - 1)
-            : (inputLength - missingSpacePos);
-    const int secondInputWordStartPos =
-            isSpaceProximity ? (spaceProximityPos + 1) : missingSpacePos;
-    int secondFreq = getMostFrequentWordLike(
-            secondInputWordStartPos, secondInputWordLength, proximityInfo, mWord);
-    unsigned short* secondOutputWord = 0;
-    int secondOutputWordLength = 0;
-
-    if (secondFreq > 0) {
-        secondOutputWordLength = secondInputWordLength;
-        secondOutputWord = mWord;
-    } else if (!hasAutoCorrectionCandidate) {
-        const int offset = secondInputWordStartPos;
-        initSuggestions(proximityInfo, &xcoordinates[offset], &ycoordinates[offset],
-                codes + offset * MAX_PROXIMITY_CHARS, secondInputWordLength, correction);
-        queuePool->clearSubQueue2();
-        getSuggestionCandidates(useFullEditDistance, secondInputWordLength, correction,
-                queuePool, false, MAX_ERRORS_FOR_TWO_WORDS, SECOND_WORD_INDEX);
-        if (DEBUG_DICT) {
-            AKLOGI("Dump second word candidates %d", secondInputWordLength);
-            for (int i = 0; i < SUB_QUEUE_MAX_COUNT; ++i) {
-                queuePool->getSubQueue2(i)->dumpTopWord();
-            }
-        }
-        WordsPriorityQueue* secondWordQueue = queuePool->getSubQueue2(secondInputWordLength);
-        if (!secondWordQueue || secondWordQueue->size() < 1) {
-            return;
-        }
-        int score = 0;
-        const double ns = secondWordQueue->getHighestNormalizedScore(
-                proximityInfo->getPrimaryInputWord(), secondInputWordLength,
-                &secondOutputWord, &score, &secondOutputWordLength);
-        if (DEBUG_DICT) {
-            AKLOGI("NS2 = %f, Score = %d", ns, score);
-        }
-        // Two words correction won't be done if the score of the first word doesn't exceed the
-        // threshold.
-        if (ns < TWO_WORDS_CORRECTION_WITH_OTHER_ERROR_THRESHOLD
-                || secondOutputWordLength < SUB_QUEUE_MIN_WORD_LENGTH) {
-            return;
-        }
-        secondFreq = score >> (secondOutputWordLength
-                + TWO_WORDS_PLUS_OTHER_ERROR_CORRECTION_DEMOTION_DIVIDER);
-    }
-
-    if (DEBUG_DICT) {
-        DUMP_WORD(secondOutputWord, secondOutputWordLength);
-        AKLOGI("Second freq: %d", secondFreq);
-    }
-
-    if (secondFreq <= 0 || secondOutputWordLength <= 0
-            || MAX_WORD_LENGTH <= (firstOutputWordLength + 1 + secondOutputWordLength)) {
+    // First word
+    int inputWordStartPos = 0;
+    int inputWordLength = isSpaceProximity ? spaceProximityPos : missingSpacePos;
+    const int firstFreq = getSubStringSuggestion(proximityInfo, xcoordinates, ycoordinates, codes,
+            useFullEditDistance, correction, queuePool, inputLength, hasAutoCorrectionCandidate,
+            FIRST_WORD_INDEX, inputWordStartPos, inputWordLength, 0, outputWord, &outputWordLength);
+    if (firstFreq <= 0) {
         return;
     }
 
-    for (int i = 0; i < secondOutputWordLength; ++i) {
-        outputWord[firstOutputWordLength + 1 + i] = secondOutputWord[i];
+    // Second word
+    inputWordStartPos = isSpaceProximity ? (spaceProximityPos + 1) : missingSpacePos;
+    inputWordLength = isSpaceProximity ? (inputLength - spaceProximityPos - 1)
+            : (inputLength - missingSpacePos);
+    const int secondFreq = getSubStringSuggestion(proximityInfo, xcoordinates, ycoordinates, codes,
+            useFullEditDistance, correction, queuePool, inputLength, hasAutoCorrectionCandidate,
+            SECOND_WORD_INDEX, inputWordStartPos, inputWordLength, outputWordLength, outputWord,
+            &outputWordLength);
+    if (secondFreq <= 0) {
+        return;
     }
 
-    outputWordLength += secondOutputWordLength;
-
     // TODO: Remove initSuggestions and correction->setCorrectionParams
     initSuggestions(proximityInfo, xcoordinates, ycoordinates, codes, inputLength, correction);
 
diff --git a/native/src/unigram_dictionary.h b/native/src/unigram_dictionary.h
index 0b82719..0f50ccb 100644
--- a/native/src/unigram_dictionary.h
+++ b/native/src/unigram_dictionary.h
@@ -127,6 +127,13 @@
             ProximityInfo *proximityInfo, unsigned short *word);
     int getMostFrequentWordLikeInner(const uint16_t* const inWord, const int length,
             short unsigned int *outWord);
+    int getSubStringSuggestion(
+            ProximityInfo *proximityInfo, const int *xcoordinates, const int *ycoordinates,
+            const int *codes, const bool useFullEditDistance, Correction *correction,
+            WordsPriorityQueuePool* queuePool, const int inputLength,
+            const bool hasAutoCorrectionCandidate, const int currentWordIndex,
+            const int inputWordStartPos, const int inputWordLength,
+            const int outputWordStartPos, unsigned short* outputWord, int *outputWordLength);
 
     const uint8_t* const DICT_ROOT;
     const int MAX_WORD_LENGTH;
diff --git a/native/src/words_priority_queue_pool.h b/native/src/words_priority_queue_pool.h
index 599b897..a4aa8b6 100644
--- a/native/src/words_priority_queue_pool.h
+++ b/native/src/words_priority_queue_pool.h
@@ -43,25 +43,24 @@
         return mMasterQueue;
     }
 
-    // TODO: Come up with more generic pool
-    WordsPriorityQueue* getSubQueue1(const int id) {
-        if (id < 0 || id >= SUB_QUEUE_MAX_COUNT) {
+    WordsPriorityQueue* getSubQueue(const int wordIndex, const int inputWordLength) {
+        if (wordIndex > SUB_QUEUE_MAX_WORD_INDEX) {
+            return 0;
+        }
+        if (inputWordLength < 0 || inputWordLength >= SUB_QUEUE_MAX_COUNT) {
             if (DEBUG_WORDS_PRIORITY_QUEUE) {
                 assert(false);
             }
             return 0;
         }
-        return mSubQueues1[id];
-    }
-
-    WordsPriorityQueue* getSubQueue2(const int id) {
-        if (id < 0 || id >= SUB_QUEUE_MAX_COUNT) {
-            if (DEBUG_WORDS_PRIORITY_QUEUE) {
-                assert(false);
-            }
+        // TODO: Come up with more generic pool
+        if (wordIndex == 1) {
+            return mSubQueues1[inputWordLength];
+        } else if (wordIndex == 2) {
+            return mSubQueues2[inputWordLength];
+        } else {
             return 0;
         }
-        return mSubQueues2[id];
     }
 
     inline void clearAll() {
@@ -72,15 +71,13 @@
         }
     }
 
-    inline void clearSubQueue1() {
+    inline void clearSubQueue(const int wordIndex) {
         for (int i = 0; i < SUB_QUEUE_MAX_COUNT; ++i) {
-            mSubQueues1[i]->clear();
-        }
-    }
-
-    inline void clearSubQueue2() {
-        for (int i = 0; i < SUB_QUEUE_MAX_COUNT; ++i) {
-            mSubQueues2[i]->clear();
+            if (wordIndex == 1) {
+                mSubQueues1[i]->clear();
+            } else if (wordIndex == 2) {
+                mSubQueues2[i]->clear();
+            }
         }
     }