merge in jb-ub-latinimegoogle-bayo history after reset to jb-ub-latinimegoogle
diff --git a/java/res/layout/emoji_keyboard_view.xml b/java/res/layout/emoji_keyboard_view.xml
index ccbcfdc..5fee419 100644
--- a/java/res/layout/emoji_keyboard_view.xml
+++ b/java/res/layout/emoji_keyboard_view.xml
@@ -76,18 +76,24 @@
         android:id="@+id/emoji_action_bar"
         android:orientation="horizontal"
         android:layout_width="match_parent"
-        android:layout_height="@dimen/suggestions_strip_height"
+        android:layout_height="0dip"
+        android:layout_weight="1"
     >
         <ImageButton
             android:id="@+id/emoji_keyboard_alphabet"
             android:layout_width="0dip"
-            android:layout_weight="0.825"
+            android:layout_weight="0.15"
             android:layout_height="match_parent"
             android:src="@drawable/ic_ime_light" />
         <ImageButton
+            android:id="@+id/emoji_keyboard_space"
+            android:layout_width="0dip"
+            android:layout_weight="0.70"
+            android:layout_height="match_parent" />
+        <ImageButton
             android:id="@+id/emoji_keyboard_send"
             android:layout_width="0dip"
-            android:layout_weight="0.125"
+            android:layout_weight="0.15"
             android:layout_height="match_parent"
             android:src="@drawable/sym_keyboard_return_holo" />
     </LinearLayout>
diff --git a/java/res/values-fr-rCA/donottranslate.xml b/java/res/values-fr-rCA/donottranslate.xml
new file mode 100644
index 0000000..21f18d8
--- /dev/null
+++ b/java/res/values-fr-rCA/donottranslate.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2009, 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.
+*/
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Symbols that are normally preceded by a space (used to add an auto-space before these) -->
+    <!-- This is similar to French with the exception of "!" "?" and ";" which do not take a space before in Canadian French. Note that ":" does take a space before according to Canadian rules. -->
+    <string name="symbols_preceded_by_space">([{&amp;:</string>
+    <!-- Symbols that are normally followed by a space (used to add an auto-space after these) -->
+    <string name="symbols_followed_by_space">.,;:!?)]}&amp;</string>
+    <!-- Symbols that separate words -->
+    <!-- Don't remove the enclosing double quotes, they protect whitespace (not just U+0020) -->
+    <string name="symbols_word_separators">"&#x0009;&#x0020;\n"()[]{}*&amp;&lt;&gt;+=|.,;:!?/_\"</string>
+    <!-- Word connectors -->
+    <string name="symbols_word_connectors">\'-</string>
+</resources>
diff --git a/java/res/values-land/dimens.xml b/java/res/values-land/dimens.xml
index c954e60..42a746b 100644
--- a/java/res/values-land/dimens.xml
+++ b/java/res/values-land/dimens.xml
@@ -75,5 +75,9 @@
     <dimen name="gesture_floating_preview_vertical_padding">15dp</dimen>
 
     <!-- Emoji keyboard -->
-    <fraction name="emoji_keyboard_key_width">8.3333%p</fraction>
+    <fraction name="emoji_keyboard_key_width">10%p</fraction>
+    <fraction name="emoji_keyboard_row_height">50%p</fraction>
+    <fraction name="emoji_keyboard_key_letter_size">60%p</fraction>
+    <integer name="emoji_keyboard_max_key_count">20</integer>
+
 </resources>
diff --git a/java/res/xml/kbd_thai_symbols.xml b/java/res/values-lo/donottranslate.xml
similarity index 66%
rename from java/res/xml/kbd_thai_symbols.xml
rename to java/res/values-lo/donottranslate.xml
index 4d9861b..a9893fe 100644
--- a/java/res/xml/kbd_thai_symbols.xml
+++ b/java/res/values-lo/donottranslate.xml
@@ -2,7 +2,7 @@
 <!--
 /*
 **
-** Copyright 2012, The Android Open Source Project
+** Copyright 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.
@@ -17,11 +17,7 @@
 ** limitations under the License.
 */
 -->
-
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
-    latin:touchPositionCorrectionData="@array/touch_position_correction_data_default"
->
-    <include
-        latin:keyboardLayout="@xml/rows_symbols" />
-</Keyboard>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Whether this language uses spaces between words -->
+    <bool name="current_language_has_spaces">false</bool>
+</resources>
diff --git a/java/res/values-sw600dp-land/dimens.xml b/java/res/values-sw600dp-land/dimens.xml
index cda462f..730b7d8 100644
--- a/java/res/values-sw600dp-land/dimens.xml
+++ b/java/res/values-sw600dp-land/dimens.xml
@@ -62,4 +62,11 @@
     <dimen name="gesture_floating_preview_text_offset">76dp</dimen>
     <dimen name="gesture_floating_preview_horizontal_padding">26dp</dimen>
     <dimen name="gesture_floating_preview_vertical_padding">17dp</dimen>
+
+    <!-- Emoji keyboard -->
+    <fraction name="emoji_keyboard_key_width">10%p</fraction>
+    <fraction name="emoji_keyboard_row_height">33%p</fraction>
+    <fraction name="emoji_keyboard_key_letter_size">95%p</fraction>
+    <integer name="emoji_keyboard_max_key_count">30</integer>
+
 </resources>
diff --git a/java/res/values-sw600dp/dimens.xml b/java/res/values-sw600dp/dimens.xml
index 7dfd0b1..2bcf2fa 100644
--- a/java/res/values-sw600dp/dimens.xml
+++ b/java/res/values-sw600dp/dimens.xml
@@ -88,4 +88,11 @@
     <dimen name="gesture_floating_preview_horizontal_padding">28dp</dimen>
     <dimen name="gesture_floating_preview_vertical_padding">19dp</dimen>
     <dimen name="gesture_floating_preview_round_radius">3dp</dimen>
+
+    <!-- Emoji keyboard -->
+    <fraction name="emoji_keyboard_key_width">12.5%p</fraction>
+    <fraction name="emoji_keyboard_row_height">33%p</fraction>
+    <fraction name="emoji_keyboard_key_letter_size">85%p</fraction>
+    <integer name="emoji_keyboard_max_key_count">24</integer>
+
 </resources>
diff --git a/java/res/values-sw768dp-land/dimens.xml b/java/res/values-sw768dp-land/dimens.xml
index 0a70480..1e2e1c6 100644
--- a/java/res/values-sw768dp-land/dimens.xml
+++ b/java/res/values-sw768dp-land/dimens.xml
@@ -63,4 +63,11 @@
     <dimen name="gesture_floating_preview_text_offset">100dp</dimen>
     <dimen name="gesture_floating_preview_horizontal_padding">32dp</dimen>
     <dimen name="gesture_floating_preview_vertical_padding">21dp</dimen>
+
+    <!-- Emoji keyboard -->
+    <fraction name="emoji_keyboard_key_width">7.69%p</fraction>
+    <fraction name="emoji_keyboard_row_height">33%p</fraction>
+    <fraction name="emoji_keyboard_key_letter_size">75%p</fraction>
+    <integer name="emoji_keyboard_max_key_count">39</integer>
+
 </resources>
diff --git a/java/res/values-sw768dp/dimens.xml b/java/res/values-sw768dp/dimens.xml
index 877e83b..f62a536 100644
--- a/java/res/values-sw768dp/dimens.xml
+++ b/java/res/values-sw768dp/dimens.xml
@@ -88,4 +88,11 @@
     <dimen name="gesture_floating_preview_horizontal_padding">26dp</dimen>
     <dimen name="gesture_floating_preview_vertical_padding">17dp</dimen>
     <dimen name="gesture_floating_preview_round_radius">3dp</dimen>
+
+    <!-- Emoji keyboard -->
+    <fraction name="emoji_keyboard_key_width">10%p</fraction>
+    <fraction name="emoji_keyboard_row_height">33%p</fraction>
+    <fraction name="emoji_keyboard_key_letter_size">85%p</fraction>
+    <integer name="emoji_keyboard_max_key_count">30</integer>
+
 </resources>
diff --git a/java/res/values-th/donottranslate.xml b/java/res/values-th/donottranslate.xml
index aeeebed..a9893fe 100644
--- a/java/res/values-th/donottranslate.xml
+++ b/java/res/values-th/donottranslate.xml
@@ -18,6 +18,6 @@
 */
 -->
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <!-- Whether this language uses spaces -->
+    <!-- Whether this language uses spaces between words -->
     <bool name="current_language_has_spaces">false</bool>
 </resources>
diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml
index 2a5334f..0978214 100644
--- a/java/res/values/attrs.xml
+++ b/java/res/values/attrs.xml
@@ -45,6 +45,8 @@
              possible states: normal, pressed, checkable, checkable+pressed, checkable+checked,
              checkable+checked+pressed. -->
         <attr name="keyBackground" format="reference" />
+        <!-- Image for the functional key used in Emoji layout. -->
+        <attr name="keyBackgroundEmojiFunctional" format="reference" />
 
         <!-- Horizontal padding of left/right aligned key label to the edge of the key. -->
         <attr name="keyLabelHorizontalPadding" format="dimension" />
diff --git a/java/res/xml/kbd_thai_symbols.xml b/java/res/values/config-additional-features.xml
similarity index 66%
copy from java/res/xml/kbd_thai_symbols.xml
copy to java/res/values/config-additional-features.xml
index 4d9861b..47eb772 100644
--- a/java/res/xml/kbd_thai_symbols.xml
+++ b/java/res/values/config-additional-features.xml
@@ -2,7 +2,7 @@
 <!--
 /*
 **
-** Copyright 2012, The Android Open Source Project
+** Copyright 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.
@@ -18,10 +18,7 @@
 */
 -->
 
-<Keyboard
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
-    latin:touchPositionCorrectionData="@array/touch_position_correction_data_default"
->
-    <include
-        latin:keyboardLayout="@xml/rows_symbols" />
-</Keyboard>
+<resources>
+    <!-- Whether phrase gestures are enabled by default -->
+    <bool name="config_default_phrase_gesture_enabled">false</bool>
+</resources>
diff --git a/java/res/values/dimens.xml b/java/res/values/dimens.xml
index 7de93e6..88e327f 100644
--- a/java/res/values/dimens.xml
+++ b/java/res/values/dimens.xml
@@ -117,6 +117,9 @@
 
     <!-- Emoji keyboard -->
     <fraction name="emoji_keyboard_key_width">14.2857%p</fraction>
+    <fraction name="emoji_keyboard_row_height">33%p</fraction>
+    <fraction name="emoji_keyboard_key_letter_size">90%p</fraction>
+    <integer name="emoji_keyboard_max_key_count">21</integer>
 
     <!-- Inset used in Accessibility mode to avoid accidental key presses when a finger slides off the screen. -->
     <dimen name="accessibility_edge_slop">8dp</dimen>
@@ -124,4 +127,5 @@
     <integer name="user_dictionary_max_word_length" translatable="false">48</integer>
 
     <dimen name="language_on_spacebar_horizontal_margin">1dp</dimen>
+
 </resources>
diff --git a/java/res/values/donottranslate.xml b/java/res/values/donottranslate.xml
index 52ebe16..82c5ce4 100644
--- a/java/res/values/donottranslate.xml
+++ b/java/res/values/donottranslate.xml
@@ -31,7 +31,7 @@
     <string name="symbols_word_separators">"&#x0009;&#x0020;\n"()[]{}*&amp;&lt;&gt;+=|.,;:!?/_\"</string>
     <!-- Word connectors -->
     <string name="symbols_word_connectors">\'-</string>
-    <!-- Whether this language uses spaces -->
+    <!-- Whether this language uses spaces between words -->
     <bool name="current_language_has_spaces">true</bool>
 
     <!--  Always show the suggestion strip -->
diff --git a/java/res/values/themes-ics.xml b/java/res/values/themes-ics.xml
index f3b6b13..19fb4fd 100644
--- a/java/res/values/themes-ics.xml
+++ b/java/res/values/themes-ics.xml
@@ -103,7 +103,7 @@
         name="EmojiKeyboardView.ICS"
         parent="KeyboardView.ICS"
     >
-        <item name="keyBackground">@drawable/btn_keyboard_key_functional_ics</item>
+        <item name="keyBackgroundEmojiFunctional">@drawable/btn_keyboard_key_functional_ics</item>
         <item name="emojiTabLabelColor">@color/emoji_tab_label_color_ics</item>
     </style>
     <style
diff --git a/java/res/xml-sw600dp/rows_lao.xml b/java/res/xml-sw600dp/rows_lao.xml
new file mode 100644
index 0000000..cfe8db9
--- /dev/null
+++ b/java/res/xml-sw600dp/rows_lao.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 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.
+*/
+-->
+
+<merge
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+>
+    <include
+        latin:keyboardLayout="@xml/key_styles_common" />
+    <Row
+        latin:keyWidth="7.5%p"
+    >
+        <include
+            latin:keyboardLayout="@xml/rowkeys_lao1" />
+        <Key
+            latin:keyStyle="deleteKeyStyle"
+            latin:keyWidth="fillRight" />
+        </Row>
+    <Row
+        latin:keyWidth="7.5%p"
+    >
+        <include
+            latin:keyboardLayout="@xml/rowkeys_lao2" />
+    </Row>
+    <Row
+        latin:keyWidth="7.5%p"
+    >
+        <include
+            latin:keyboardLayout="@xml/rowkeys_lao3" />
+        <Key
+            latin:keyStyle="enterKeyStyle"
+            latin:keyWidth="fillRight" />
+    </Row>
+    <Row
+        latin:keyWidth="7.5%p"
+    >
+        <Key
+            latin:keyStyle="shiftKeyStyle"
+            latin:keyWidth="10.0%p" />
+        <include
+            latin:keyboardLayout="@xml/rowkeys_lao4" />
+        <include
+            latin:keyboardLayout="@xml/keys_exclamation_question" />
+    </Row>
+    <include
+        latin:keyboardLayout="@xml/row_qwerty4" />
+</merge>
diff --git a/java/res/xml/kbd_emoji_category1.xml b/java/res/xml/kbd_emoji_category1.xml
index 92b0c3f..c11a830 100644
--- a/java/res/xml/kbd_emoji_category1.xml
+++ b/java/res/xml/kbd_emoji_category1.xml
@@ -22,6 +22,7 @@
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
     latin:keyWidth="@fraction/emoji_keyboard_key_width"
     latin:keyLetterSize="90%p"
+    latin:rowHeight="@fraction/emoji_keyboard_row_height"
 >
     <GridRows
         latin:codesArray="@array/emoji_faces"
diff --git a/java/res/xml/kbd_emoji_category2.xml b/java/res/xml/kbd_emoji_category2.xml
index 17d36c5..d3e5890 100644
--- a/java/res/xml/kbd_emoji_category2.xml
+++ b/java/res/xml/kbd_emoji_category2.xml
@@ -22,6 +22,7 @@
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
     latin:keyWidth="@fraction/emoji_keyboard_key_width"
     latin:keyLetterSize="90%p"
+    latin:rowHeight="@fraction/emoji_keyboard_row_height"
 >
     <GridRows
         latin:codesArray="@array/emoji_objects"
diff --git a/java/res/xml/kbd_emoji_category3.xml b/java/res/xml/kbd_emoji_category3.xml
index 9000a3a..0efafa8 100644
--- a/java/res/xml/kbd_emoji_category3.xml
+++ b/java/res/xml/kbd_emoji_category3.xml
@@ -22,6 +22,7 @@
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
     latin:keyWidth="@fraction/emoji_keyboard_key_width"
     latin:keyLetterSize="90%p"
+    latin:rowHeight="@fraction/emoji_keyboard_row_height"
 >
     <GridRows
         latin:codesArray="@array/emoji_nature"
diff --git a/java/res/xml/kbd_emoji_category4.xml b/java/res/xml/kbd_emoji_category4.xml
index e79e124..e529120 100644
--- a/java/res/xml/kbd_emoji_category4.xml
+++ b/java/res/xml/kbd_emoji_category4.xml
@@ -22,6 +22,7 @@
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
     latin:keyWidth="@fraction/emoji_keyboard_key_width"
     latin:keyLetterSize="90%p"
+    latin:rowHeight="@fraction/emoji_keyboard_row_height"
 >
     <GridRows
         latin:codesArray="@array/emoji_places"
diff --git a/java/res/xml/kbd_emoji_category5.xml b/java/res/xml/kbd_emoji_category5.xml
index 07b3d90..1836879 100644
--- a/java/res/xml/kbd_emoji_category5.xml
+++ b/java/res/xml/kbd_emoji_category5.xml
@@ -22,6 +22,7 @@
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
     latin:keyWidth="@fraction/emoji_keyboard_key_width"
     latin:keyLetterSize="90%p"
+    latin:rowHeight="@fraction/emoji_keyboard_row_height"
 >
     <GridRows
         latin:codesArray="@array/emoji_symbols"
diff --git a/java/res/xml/kbd_emoji_category6.xml b/java/res/xml/kbd_emoji_category6.xml
index 838f3f5..b47ebfe 100644
--- a/java/res/xml/kbd_emoji_category6.xml
+++ b/java/res/xml/kbd_emoji_category6.xml
@@ -23,6 +23,7 @@
     latin:keyWidth="@fraction/emoji_keyboard_key_width"
     latin:keyLetterSize="90%p"
     latin:keyLabelSize="60%p"
+    latin:rowHeight="@fraction/emoji_keyboard_row_height"
 >
     <GridRows
         latin:textsArray="@array/emoji_emoticons"
diff --git a/java/res/xml/kbd_emoji_recents.xml b/java/res/xml/kbd_emoji_recents.xml
index f56b79a..73926ec 100644
--- a/java/res/xml/kbd_emoji_recents.xml
+++ b/java/res/xml/kbd_emoji_recents.xml
@@ -21,8 +21,9 @@
 <Keyboard
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
     latin:keyWidth="@fraction/emoji_keyboard_key_width"
-    latin:keyLetterSize="90%p"
+    latin:keyLetterSize="@fraction/emoji_keyboard_key_letter_size"
     latin:keyLabelSize="60%p"
+    latin:rowHeight="@fraction/emoji_keyboard_row_height"
 >
     <GridRows
         latin:codesArray="@array/emoji_recents"
diff --git a/java/res/xml/kbd_thai_symbols_shift.xml b/java/res/xml/kbd_lao.xml
similarity index 71%
rename from java/res/xml/kbd_thai_symbols_shift.xml
rename to java/res/xml/kbd_lao.xml
index a2d67ca..2bba330 100644
--- a/java/res/xml/kbd_thai_symbols_shift.xml
+++ b/java/res/xml/kbd_lao.xml
@@ -2,7 +2,7 @@
 <!--
 /*
 **
-** Copyright 2012, The Android Open Source Project
+** Copyright 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.
@@ -20,8 +20,12 @@
 
 <Keyboard
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+    latin:rowHeight="20%p"
+    latin:verticalGap="@fraction/key_bottom_gap_5row"
+    latin:keyLetterSize="@fraction/key_letter_ratio_5row"
+    latin:keyShiftedLetterHintRatio="@fraction/key_uppercase_letter_ratio_5row"
     latin:touchPositionCorrectionData="@array/touch_position_correction_data_default"
 >
     <include
-        latin:keyboardLayout="@xml/rows_symbols_shift" />
+        latin:keyboardLayout="@xml/rows_lao" />
 </Keyboard>
diff --git a/java/res/xml-sw600dp/key_f2.xml b/java/res/xml/key_f2.xml
similarity index 83%
rename from java/res/xml-sw600dp/key_f2.xml
rename to java/res/xml/key_f2.xml
index 674153a..473dd21 100644
--- a/java/res/xml-sw600dp/key_f2.xml
+++ b/java/res/xml/key_f2.xml
@@ -26,11 +26,13 @@
             latin:mode="email|url"
         >
             <Key
-                latin:keyStyle="comKeyStyle" />
+                latin:keyStyle="comKeyStyle"
+                latin:keyWidth="fillRight" />
         </case>
         <default>
             <Key
-                latin:keyStyle="emojiKeyStyle" />
+                latin:keyStyle="emojiKeyStyle"
+                latin:keyWidth="fillRight" />
         </default>
     </switch>
 </merge>
diff --git a/java/res/xml/key_space_5kw.xml b/java/res/xml/key_space_5kw.xml
index 02ee42f..b6d38fb 100644
--- a/java/res/xml/key_space_5kw.xml
+++ b/java/res/xml/key_space_5kw.xml
@@ -23,7 +23,7 @@
 >
     <switch>
         <case
-            latin:languageCode="fa"
+            latin:languageCode="fa|ne"
             latin:languageSwitchKeyEnabled="true"
         >
             <Key
@@ -35,7 +35,7 @@
                 latin:keyStyle="zwnjKeyStyle" />
         </case>
         <case
-            latin:languageCode="fa"
+            latin:languageCode="fa|ne"
             latin:languageSwitchKeyEnabled="false"
         >
             <Key
diff --git a/java/res/xml/key_styles_currency.xml b/java/res/xml/key_styles_currency.xml
index 60333ee..b7677a2 100644
--- a/java/res/xml/key_styles_currency.xml
+++ b/java/res/xml/key_styles_currency.xml
@@ -95,14 +95,16 @@
         <!-- fa: Persian (Rial and Afgahni)
              hi: Hindi (Indian Rupee)
              iw: Hebrew (New Sheqel)
+             lo: Lao (Kip)
              mn: Mongolian (Tugrik)
+             ne: Nepali (Nepalese Rupee)
              th: Thai (Baht)
              uk: Ukrainian (Hryvnia)
              vi: Vietnamese (Dong)  -->
         <!-- TODO: The currency sign of Turkish Lira was created in 2012 and assigned U+20BA for
              its unicode, although there is no font glyph for it as of November 2012. -->
         <case
-            latin:languageCode="fa|hi|iw|mn|th|uk|vi"
+            latin:languageCode="fa|hi|iw|lo|mn|ne|th|uk|vi"
         >
             <!-- U+00A3: "£" POUND SIGN
                  U+20AC: "€" EURO SIGN
diff --git a/java/res/xml/keyboard_layout_set_armenian_phonetic.xml b/java/res/xml/keyboard_layout_set_armenian_phonetic.xml
index b374fae..35bd43f 100644
--- a/java/res/xml/keyboard_layout_set_armenian_phonetic.xml
+++ b/java/res/xml/keyboard_layout_set_armenian_phonetic.xml
@@ -28,6 +28,9 @@
         latin:elementName="symbols"
         latin:elementKeyboard="@xml/kbd_symbols" />
     <Element
+        latin:elementName="symbolsShifted"
+        latin:elementKeyboard="@xml/kbd_symbols_shift" />
+    <Element
         latin:elementName="phone"
         latin:elementKeyboard="@xml/kbd_phone" />
     <Element
diff --git a/java/res/xml/keyboard_layout_set_lao.xml b/java/res/xml/keyboard_layout_set_lao.xml
new file mode 100644
index 0000000..2ffde45
--- /dev/null
+++ b/java/res/xml/keyboard_layout_set_lao.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 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.
+*/
+-->
+
+<KeyboardLayoutSet
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin">
+    <Element
+        latin:elementName="alphabet"
+        latin:elementKeyboard="@xml/kbd_lao"
+        latin:enableProximityCharsCorrection="true" />
+    <Element
+        latin:elementName="alphabetAutomaticShifted"
+        latin:elementKeyboard="@xml/kbd_lao"
+        latin:enableProximityCharsCorrection="true" />
+    <!-- On these shifted alphabet layouts the proximity characters correction should be disabled
+         because the letters on these layouts aren't the ones in different case of the above
+         unshifted layouts. -->
+    <Element
+        latin:elementName="alphabetManualShifted"
+        latin:elementKeyboard="@xml/kbd_lao" />
+    <Element
+        latin:elementName="alphabetShiftLocked"
+        latin:elementKeyboard="@xml/kbd_lao" />
+    <Element
+        latin:elementName="alphabetShiftLockShifted"
+        latin:elementKeyboard="@xml/kbd_lao" />
+    <Element
+        latin:elementName="symbols"
+        latin:elementKeyboard="@xml/kbd_symbols" />
+    <Element
+        latin:elementName="symbolsShifted"
+        latin:elementKeyboard="@xml/kbd_symbols_shift" />
+    <Element
+        latin:elementName="phone"
+        latin:elementKeyboard="@xml/kbd_phone" />
+    <Element
+        latin:elementName="phoneSymbols"
+        latin:elementKeyboard="@xml/kbd_phone_symbols" />
+    <Element
+        latin:elementName="number"
+        latin:elementKeyboard="@xml/kbd_number" />
+</KeyboardLayoutSet>
diff --git a/java/res/xml/keyboard_layout_set_nepali_romanized.xml b/java/res/xml/keyboard_layout_set_nepali_romanized.xml
index 82f36cf..fbbc6a5 100644
--- a/java/res/xml/keyboard_layout_set_nepali_romanized.xml
+++ b/java/res/xml/keyboard_layout_set_nepali_romanized.xml
@@ -44,6 +44,9 @@
         latin:elementName="symbols"
         latin:elementKeyboard="@xml/kbd_symbols" />
     <Element
+        latin:elementName="symbolsShifted"
+        latin:elementKeyboard="@xml/kbd_symbols_shift" />
+    <Element
         latin:elementName="phone"
         latin:elementKeyboard="@xml/kbd_phone" />
     <Element
diff --git a/java/res/xml/keyboard_layout_set_nepali_traditional.xml b/java/res/xml/keyboard_layout_set_nepali_traditional.xml
index 2a6dc8e..4a3b601 100644
--- a/java/res/xml/keyboard_layout_set_nepali_traditional.xml
+++ b/java/res/xml/keyboard_layout_set_nepali_traditional.xml
@@ -44,6 +44,9 @@
         latin:elementName="symbols"
         latin:elementKeyboard="@xml/kbd_symbols" />
     <Element
+        latin:elementName="symbolsShifted"
+        latin:elementKeyboard="@xml/kbd_symbols_shift" />
+    <Element
         latin:elementName="phone"
         latin:elementKeyboard="@xml/kbd_phone" />
     <Element
diff --git a/java/res/xml/keyboard_layout_set_thai.xml b/java/res/xml/keyboard_layout_set_thai.xml
index 94713e3..b8f9997 100644
--- a/java/res/xml/keyboard_layout_set_thai.xml
+++ b/java/res/xml/keyboard_layout_set_thai.xml
@@ -42,10 +42,10 @@
         latin:elementKeyboard="@xml/kbd_thai" />
     <Element
         latin:elementName="symbols"
-        latin:elementKeyboard="@xml/kbd_thai_symbols" />
+        latin:elementKeyboard="@xml/kbd_symbols" />
     <Element
         latin:elementName="symbolsShifted"
-        latin:elementKeyboard="@xml/kbd_thai_symbols_shift" />
+        latin:elementKeyboard="@xml/kbd_symbols_shift" />
     <Element
         latin:elementName="phone"
         latin:elementKeyboard="@xml/kbd_phone" />
diff --git a/java/res/xml/method.xml b/java/res/xml/method.xml
index c3d68c6..6014646 100644
--- a/java/res/xml/method.xml
+++ b/java/res/xml/method.xml
@@ -55,6 +55,7 @@
     ka: Georgian/georgian
     (kk: Kazakh/east_slavic) # disabled temporarily. waiting for strnig resources.
     ky: Kyrgyz/east_slavic
+    lo: Lao/lao
     lt: Lithuanian/qwerty
     lv: Latvian/qwerty
     mk: Macedonian/south_slavic
@@ -332,6 +333,13 @@
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:subtypeId="0x8315772c"
+            android:imeSubtypeLocale="lo"
+            android:imeSubtypeMode="keyboard"
+            android:imeSubtypeExtraValue="KeyboardLayoutSet=lao"
+    />
+    <subtype android:icon="@drawable/ic_subtype_keyboard"
+            android:label="@string/subtype_generic"
             android:subtypeId="0x8321bb43"
             android:imeSubtypeLocale="lt"
             android:imeSubtypeMode="keyboard"
diff --git a/java/res/xml/row_symbols_shift4.xml b/java/res/xml/row_symbols_shift4.xml
index 57a2ec0..72d24a3 100644
--- a/java/res/xml/row_symbols_shift4.xml
+++ b/java/res/xml/row_symbols_shift4.xml
@@ -24,8 +24,7 @@
         latin:keyboardLayout="@xml/key_space_symbols" />
     <include latin:keyboardLayout="@xml/keys_comma_period" />
 
-    <Key
-        latin:keyStyle="enterKeyStyle"
-        latin:keyWidth="fillRight" />
+    <include
+        latin:keyboardLayout="@xml/key_f2" />
 
 </merge>
diff --git a/java/res/xml/rowkeys_lao1.xml b/java/res/xml/rowkeys_lao1.xml
new file mode 100644
index 0000000..fa1ad97
--- /dev/null
+++ b/java/res/xml/rowkeys_lao1.xml
@@ -0,0 +1,164 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 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.
+*/
+-->
+
+<merge
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+>
+    <switch>
+        <case
+            latin:keyboardLayoutSetElement="alphabetManualShifted|alphabetShiftLocked|alphabetShiftLockShifted"
+        >
+            <!-- U+0ED1: "໑" LAO DIGIT ONE -->
+            <Key
+                latin:keyLabel="&#x0ED1;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0ED2: "໒" LAO DIGIT TWO -->
+            <Key
+                latin:keyLabel="&#x0ED2;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0ED3: "໓" LAO DIGIT THREE -->
+            <Key
+                latin:keyLabel="&#x0ED3;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0ED4: "໔" LAO DIGIT FOUR -->
+            <Key
+                latin:keyLabel="&#x0ED4;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0ECC: "໌" LAO CANCELLATION MARK -->
+            <Key
+                latin:keyLabel="&#x0ECC;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EBC: "ຼ" LAO SEMIVOWEL SIGN LO -->
+            <Key
+                latin:keyLabel="&#x0EBC;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0ED5: "໕" LAO DIGIT FIVE -->
+            <Key
+                latin:keyLabel="&#x0ED5;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0ED6: "໖" LAO DIGIT SIX -->
+            <Key
+                latin:keyLabel="&#x0ED6;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0ED7: "໗" LAO DIGIT SEVEN -->
+            <Key
+                latin:keyLabel="&#x0ED7;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0ED8: "໘" LAO DIGIT EIGHT -->
+            <Key
+                latin:keyLabel="&#x0ED8;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0ED9: "໙" LAO DIGIT NINE -->
+            <Key
+                latin:keyLabel="&#x0ED9;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0ECD/U+0EC8: "ໍ່" LAO NIGGAHITA/LAO TONE MAI EK -->
+            <Key
+                latin:keyLabel="&#x0ECD;&#x0EC8;"
+                latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
+        </case>
+        <default>
+            <!-- U+0EA2: "ຢ" LAO LETTER YO
+                 U+0ED1: "໑" LAO DIGIT ONE -->
+            <Key
+                latin:keyLabel="&#x0EA2;"
+                latin:keyHintLabel="1"
+                latin:additionalMoreKeys="1"
+                latin:moreKeys="&#x0ED1;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E9F: "ຟ" LAO LETTER FO SUNG
+                 U+0ED2: "໒" LAO DIGIT TWO -->
+            <Key
+                latin:keyLabel="&#x0E9F;"
+                latin:keyHintLabel="2"
+                latin:additionalMoreKeys="2"
+                latin:moreKeys="&#x0ED2;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EC2: "ໂ" LAO VOWEL SIGN O
+                 U+0ED3: "໓" LAO DIGIT THREE -->
+            <Key
+                latin:keyLabel="&#x0EC2;"
+                latin:keyHintLabel="3"
+                latin:additionalMoreKeys="3"
+                latin:moreKeys="&#x0ED3;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E96: "ຖ" LAO LETTER THO SUNG
+                 U+0ED4: "໔" LAO DIGIT FOUR -->
+            <Key
+                latin:keyLabel="&#x0E96;"
+                latin:keyHintLabel="4"
+                latin:additionalMoreKeys="4"
+                latin:moreKeys="&#x0ED4;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EB8: "ຸ" LAO VOWEL SIGN U -->
+            <Key
+                latin:keyLabel="&#x0EB8;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EB9: "ູ" LAO VOWEL SIGN UU -->
+            <Key
+                latin:keyLabel="&#x0EB9;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E84: "ຄ" LAO LETTER KHO TAM
+                 U+0ED5: "໕" LAO DIGIT FIVE -->
+            <Key
+                latin:keyLabel="&#x0E84;"
+                latin:keyHintLabel="5"
+                latin:additionalMoreKeys="5"
+                latin:moreKeys="&#x0ED5;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E95: "ຕ" LAO LETTER TO
+                 U+0ED6: "໖" LAO DIGIT SIX -->
+            <Key
+                latin:keyLabel="&#x0E95;"
+                latin:keyHintLabel="6"
+                latin:additionalMoreKeys="6"
+                latin:moreKeys="&#x0ED6;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E88: "ຈ" LAO LETTER CO
+                 U+0ED7: "໗" LAO DIGIT SEVEN -->
+            <Key
+                latin:keyLabel="&#x0E88;"
+                latin:keyHintLabel="7"
+                latin:additionalMoreKeys="7"
+                latin:moreKeys="&#x0ED7;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E82: "ຂ" LAO LETTER KHO SUNG
+                 U+0ED8: "໘" LAO DIGIT EIGHT -->
+            <Key
+                latin:keyLabel="&#x0E82;"
+                latin:keyHintLabel="8"
+                latin:additionalMoreKeys="8"
+                latin:moreKeys="&#x0ED8;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E8A: "ຊ" LAO LETTER SO TAM
+                 U+0ED9: "໙" LAO DIGIT NINE -->
+            <Key
+                latin:keyLabel="&#x0E8A;"
+                latin:keyHintLabel="9"
+                latin:additionalMoreKeys="9"
+                latin:moreKeys="&#x0ED9;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0ECD: "ໍ" LAO NIGGAHITA -->
+            <Key
+                latin:keyLabel="&#x0ECD;"
+                latin:keyLabelFlags="fontNormal" />
+        </default>
+    </switch>
+</merge>
diff --git a/java/res/xml/rowkeys_lao2.xml b/java/res/xml/rowkeys_lao2.xml
new file mode 100644
index 0000000..fca58ac
--- /dev/null
+++ b/java/res/xml/rowkeys_lao2.xml
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 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.
+*/
+-->
+
+<merge
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+>
+    <switch>
+        <case
+            latin:keyboardLayoutSetElement="alphabetManualShifted|alphabetShiftLocked|alphabetShiftLockShifted"
+        >
+            <!-- U+0EBB/U+0EC9: "" LAO VOWEL SIGN MAI KON/LAO TONE MAI THO -->
+            <Key
+                latin:keyLabel="&#x0EBB;&#x0EC9;"
+                latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
+            <!-- U+0ED0: "໐" LAO DIGIT ZERO -->
+            <Key
+                latin:keyLabel="&#x0ED0;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EB3/U+0EC9: "ຳ້" LAO VOWEL SIGN AM/LAO TONE MAI THO -->
+            <Key
+                latin:keyLabel="&#x0EB3;&#x0EC9;"
+                latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
+            <Key
+                latin:keyLabel="_" />
+            <Key
+                latin:keyLabel="+" />
+            <!-- U+0EB4/U+0EC9: "ິ້" LAO VOWEL SIGN I/LAO TONE MAI THO -->
+            <Key
+                latin:keyLabel="&#x0EB4;&#x0EC9;"
+                latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
+            <!-- U+0EB5/U+0EC9: "ີ້" LAO VOWEL SIGN II/LAO TONE MAI THO -->
+            <Key
+                latin:keyLabel="&#x0EB5;&#x0EC9;"
+                latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
+            <!-- U+0EA3: "ຣ" LAO LETTER LO LING -->
+            <Key
+                latin:keyLabel="&#x0EA3;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EDC: "ໜ" LAO HO NO -->
+            <Key
+                latin:keyLabel="&#x0EDC;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EBD: "ຽ" LAO SEMIVOWEL SIGN NYO -->
+            <Key
+                latin:keyLabel="&#x0EBD;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EAB/U+0EBC: "" LAO LETTER HO SUNG/LAO SEMIVOWEL SIGN LO -->
+            <Key
+                latin:keyLabel="&#x0EAB;&#x0EBC;"
+                latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
+            <!-- U+201D: "”" RIGHT DOUBLE QUOTATION MARK -->
+            <Key
+                latin:keyLabel="&#x201D;" />
+        </case>
+        <default>
+            <!-- U+0EBB: "ົ" LAO VOWEL SIGN MAI KON -->
+            <Key
+                latin:keyLabel="&#x0EBB;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EC4: "ໄ" LAO VOWEL SIGN AI
+                 U+0ED0: "໐" LAO DIGIT ZERO -->
+            <Key
+                latin:keyLabel="&#x0EC4;"
+                latin:keyHintLabel="0"
+                latin:additionalMoreKeys="0"
+                latin:moreKeys="&#x0ED0;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EB3: "ຳ" LAO VOWEL SIGN AM -->
+            <Key
+                latin:keyLabel="&#x0EB3;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E9E: "ພ" LAO LETTER PHO TAM -->
+            <Key
+                latin:keyLabel="&#x0E9E;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EB0: "ະ" LAO VOWEL SIGN A -->
+            <Key
+                latin:keyLabel="&#x0EB0;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EB4: "ິ" LAO VOWEL SIGN I -->
+            <Key
+                latin:keyLabel="&#x0EB4;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EB5: "ີ" LAO VOWEL SIGN II -->
+            <Key
+                latin:keyLabel="&#x0EB5;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EAE: "ຮ" LAO LETTER HO TAM -->
+            <Key
+                latin:keyLabel="&#x0EAE;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E99: "ນ" LAO LETTER NO -->
+            <Key
+                latin:keyLabel="&#x0E99;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E8D: "ຍ" LAO LETTER NYO -->
+            <Key
+                latin:keyLabel="&#x0E8D;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E9A: "ບ" LAO LETTER BO -->
+            <Key
+                latin:keyLabel="&#x0E9A;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EA5: "ລ" LAO LETTER LO LOOT -->
+            <Key
+                latin:keyLabel="&#x0EA5;"
+                latin:keyLabelFlags="fontNormal" />
+        </default>
+    </switch>
+</merge>
diff --git a/java/res/xml/rowkeys_lao3.xml b/java/res/xml/rowkeys_lao3.xml
new file mode 100644
index 0000000..2a6c2d1
--- /dev/null
+++ b/java/res/xml/rowkeys_lao3.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 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.
+*/
+-->
+
+<merge
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+>
+    <switch>
+        <case
+            latin:keyboardLayoutSetElement="alphabetManualShifted|alphabetShiftLocked|alphabetShiftLockShifted"
+        >
+            <!-- U+0EB1/U+0EC9: "ັ້" LAO VOWEL SIGN MAI KAN/LAO TONE MAI THO -->
+            <Key
+                latin:keyLabel="&#x0EB1;&#x0EC9;"
+                latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
+            <Key
+                latin:keyLabel=";" />
+            <Key
+                latin:keyLabel="." />
+            <Key
+                latin:keyLabel="," />
+            <Key
+                latin:keyLabel=":" />
+            <!-- U+0ECA: "໊" LAO TONE MAI TI -->
+            <Key
+                latin:keyLabel="&#x0ECA;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0ECB: "໋" LAO TONE MAI CATAWA -->
+            <Key
+                latin:keyLabel="&#x0ECB;"
+                latin:keyLabelFlags="fontNormal" />
+            <Key
+                latin:keyLabel="!" />
+            <Key
+                latin:keyLabel="\?" />
+            <Key
+                latin:keyLabel="%" />
+            <Key
+                latin:keyLabel="=" />
+            <!-- U+201C: "“" LEFT DOUBLE QUOTATION MARK -->
+            <Key
+                latin:keyLabel="&#x201C;" />
+        </case>
+        <default>
+            <!-- U+0EB1: "ັ" LAO VOWEL SIGN MAI KAN -->
+            <Key
+                latin:keyLabel="&#x0EB1;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EAB: "ຫ" LAO LETTER HO SUNG -->
+            <Key
+                latin:keyLabel="&#x0EAB;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E81: "ກ" LAO LETTER KO -->
+            <Key
+                latin:keyLabel="&#x0E81;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E94: "ດ" LAO LETTER DO -->
+            <Key
+                latin:keyLabel="&#x0E94;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EC0: "ເ" LAO VOWEL SIGN E -->
+            <Key
+                latin:keyLabel="&#x0EC0;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EC9: "້" LAO TONE MAI THO -->
+            <Key
+                latin:keyLabel="&#x0EC9;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EC8: "່" LAO TONE MAI EK -->
+            <Key
+                latin:keyLabel="&#x0EC8;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EB2: "າ" LAO VOWEL SIGN AA -->
+            <Key
+                latin:keyLabel="&#x0EB2;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EAA: "ສ" LAO LETTER SO SUNG -->
+            <Key
+                latin:keyLabel="&#x0EAA;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EA7: "ວ" LAO LETTER WO -->
+            <Key
+                latin:keyLabel="&#x0EA7;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E87: "ງ" LAO LETTER NGO -->
+            <Key
+                latin:keyLabel="&#x0E87;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+201C: "“" LEFT DOUBLE QUOTATION MARK -->
+            <Key
+                latin:keyLabel="&#x201C;" />
+        </default>
+    </switch>
+</merge>
diff --git a/java/res/xml/rowkeys_lao4.xml b/java/res/xml/rowkeys_lao4.xml
new file mode 100644
index 0000000..fae9cc9
--- /dev/null
+++ b/java/res/xml/rowkeys_lao4.xml
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 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.
+*/
+-->
+
+<merge
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+>
+    <switch>
+        <case
+            latin:keyboardLayoutSetElement="alphabetManualShifted|alphabetShiftLocked|alphabetShiftLockShifted"
+        >
+            <!-- U+20AD: "₭" KIP SIGN -->
+            <Key
+                latin:keyLabel="&#x20AD;" />
+            <Key
+                latin:keyLabel="(" />
+            <!-- U+0EAF: "ຯ" LAO ELLIPSIS -->
+            <Key
+                latin:keyLabel="&#x0EAF;"
+                latin:keyLabelFlags="fontNormal" />
+            <Key
+                latin:keyLabel="\@" />
+            <!-- U+0EB6/U+0EC9: "ຶ້" LAO VOWEL SIGN Y/LAO TONE MAI THO -->
+            <Key
+                latin:keyLabel="&#x0EB6;&#x0EC9;"
+                latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
+            <!-- U+0EB7/U+0EC9: "ື້" LAO VOWEL SIGN YY/LAO TONE MAI THO -->
+            <Key
+                latin:keyLabel="&#x0EB7;&#x0EC9;"
+                latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
+            <!-- U+0EC6: "ໆ" LAO KO LA -->
+            <Key
+                latin:keyLabel="&#x0EC6;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EDD: "ໝ" LAO HO MO -->
+            <Key
+                latin:keyLabel="&#x0EDD;"
+                latin:keyLabelFlags="fontNormal" />
+            <Key
+                latin:keyLabel="$" />
+            <Key
+                latin:keyLabel=")" />
+        </case>
+        <default>
+            <!-- U+0E9C: "ຜ" LAO LETTER PHO SUNG -->
+            <Key
+                latin:keyLabel="&#x0E9C;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E9B: "ປ" LAO LETTER PO -->
+            <Key
+                latin:keyLabel="&#x0E9B;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EC1: "ແ" LAO VOWEL SIGN EI -->
+            <Key
+                latin:keyLabel="&#x0EC1;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EAD: "ອ" LAO LETTER O -->
+            <Key
+                latin:keyLabel="&#x0EAD;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EB6: "ຶ" LAO VOWEL SIGN Y -->
+            <Key
+                latin:keyLabel="&#x0EB6;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EB7: "ື" LAO VOWEL SIGN YY -->
+            <Key
+                latin:keyLabel="&#x0EB7;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E97: "ທ" LAO LETTER THO TAM -->
+            <Key
+                latin:keyLabel="&#x0E97;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EA1: "ມ" LAO LETTER MO -->
+            <Key
+                latin:keyLabel="&#x0EA1;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0EC3: "ໃ" LAO VOWEL SIGN AY -->
+            <Key
+                latin:keyLabel="&#x0EC3;"
+                latin:keyLabelFlags="fontNormal" />
+            <!-- U+0E9D: "ຝ" LAO LETTER FO TAM -->
+            <Key
+                latin:keyLabel="&#x0E9D;"
+                latin:keyLabelFlags="fontNormal" />
+        </default>
+    </switch>
+</merge>
diff --git a/java/res/xml/rowkeys_nepali_romanized3.xml b/java/res/xml/rowkeys_nepali_romanized3.xml
index 5660596..166d028 100644
--- a/java/res/xml/rowkeys_nepali_romanized3.xml
+++ b/java/res/xml/rowkeys_nepali_romanized3.xml
@@ -48,7 +48,7 @@
                 latin:keyLabelFlags="fontNormal" />
             <!-- U+0923: "ण" DEVANAGARI LETTER NNA -->
             <Key
-                latin:keyLabel="&#x0936;"
+                latin:keyLabel="&#x0923;"
                 latin:keyLabelFlags="fontNormal" />
             <!-- Because the font rendering system prior to API version 16 can't automatically
                  render dotted circle for incomplete combining letter of some scripts, different
diff --git a/java/res/xml/rowkeys_nepali_traditional2.xml b/java/res/xml/rowkeys_nepali_traditional2.xml
index 2c53b3b..45620a9 100644
--- a/java/res/xml/rowkeys_nepali_traditional2.xml
+++ b/java/res/xml/rowkeys_nepali_traditional2.xml
@@ -29,13 +29,13 @@
             <Key
                 latin:keyLabel="&#x0906;"
                 latin:keyLabelFlags="fontNormal" />
-            <!-- U+0919/U+094D/U+0915: "ङ्क" DEVANAGARI LETTER NGA/DEVANAGARI SIGN VIRAMA/DEVANAGARI LETTER KA -->
+            <!-- U+0919/U+094D: "ङ्" DEVANAGARI LETTER NGA/DEVANAGARI SIGN VIRAMA -->
             <Key
-                latin:keyLabel="&#x0919;&#x094D;&#x0915;"
+                latin:keyLabel="&#x0919;&#x094D;"
                 latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
-            <!-- U+0919/U+094D/U+0917: "ङ्ग" DEVANAGARI LETTER NGA/DEVANAGARI SIGN VIRAMA/DEVANAGARI LETTER GA -->
+            <!-- U+0921/U+094D/U+0921: "ड्ड" DEVANAGARI LETTER DDA/DEVANAGARI SIGN VIRAMA/DEVANAGARI LETTER DDA -->
             <Key
-                latin:keyLabel="&#x0919;&#x094D;&#x0917;"
+                latin:keyLabel="&#x0921;&#x094D;&#x0921;"
                 latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
             <!-- Because the font rendering system prior to API version 16 can't automatically
                  render dotted circle for incomplete combining letter of some scripts, different
diff --git a/java/res/xml/rowkeys_nepali_traditional3_left6.xml b/java/res/xml/rowkeys_nepali_traditional3_left6.xml
index d4388e0..1cacced 100644
--- a/java/res/xml/rowkeys_nepali_traditional3_left6.xml
+++ b/java/res/xml/rowkeys_nepali_traditional3_left6.xml
@@ -25,9 +25,9 @@
         <case
             latin:keyboardLayoutSetElement="alphabetManualShifted|alphabetShiftLocked|alphabetShiftLockShifted"
         >
-            <!-- U+0915/U+094D/U+0915: "क्क" DEVANAGARI LETTER KA/DEVANAGARI SIGN VIRAMA/DEVANAGARI LETTER KA -->
+            <!-- U+0915/U+094D: "क्" DEVANAGARI LETTER KA/DEVANAGARI SIGN VIRAMA -->
             <Key
-                latin:keyLabel="&#x0915;&#x094D;&#x0915;"
+                latin:keyLabel="&#x0915;&#x094D;"
                 latin:keyLabelFlags="fontNormal|followKeyLetterRatio" />
             <!-- U+0939/U+094D/U+092E: "ह्म" DEVANAGARI LETTER HA/DEVANAGARI SIGN VIRAMA/DEVANAGARI LETTER MA -->
             <Key
diff --git a/java/res/xml/rows_lao.xml b/java/res/xml/rows_lao.xml
new file mode 100644
index 0000000..321f411
--- /dev/null
+++ b/java/res/xml/rows_lao.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 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.
+*/
+-->
+
+<merge
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+>
+    <include
+        latin:keyboardLayout="@xml/key_styles_common" />
+    <Row
+        latin:keyWidth="8.3333%p"
+    >
+        <include
+            latin:keyboardLayout="@xml/rowkeys_lao1" />
+    </Row>
+    <Row
+        latin:keyWidth="8.3333%p"
+    >
+        <include
+            latin:keyboardLayout="@xml/rowkeys_lao2" />
+    </Row>
+    <Row
+        latin:keyWidth="8.3333%p"
+    >
+        <include
+            latin:keyboardLayout="@xml/rowkeys_lao3" />
+    </Row>
+    <Row
+        latin:keyWidth="8.3333%p"
+    >
+        <Key
+            latin:keyStyle="shiftKeyStyle" />
+        <include
+            latin:keyboardLayout="@xml/rowkeys_lao4" />
+        <Key
+            latin:keyStyle="deleteKeyStyle" />
+    </Row>
+    <include
+        latin:keyboardLayout="@xml/row_qwerty4" />
+</merge>
diff --git a/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java b/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java
index 25ff8d0..702ed20 100644
--- a/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java
@@ -19,12 +19,18 @@
 import static com.android.inputmethod.latin.Constants.NOT_A_COORDINATE;
 
 import android.content.Context;
+import android.content.SharedPreferences;
 import android.content.res.ColorStateList;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.os.Build;
+import android.preference.PreferenceManager;
 import android.support.v4.view.PagerAdapter;
 import android.support.v4.view.ViewPager;
 import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Pair;
 import android.util.SparseArray;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -35,16 +41,21 @@
 import android.widget.TabHost.OnTabChangeListener;
 import android.widget.TextView;
 
-import com.android.inputmethod.keyboard.internal.RecentsKeyboard;
+import com.android.inputmethod.keyboard.internal.DynamicGridKeyboard;
 import com.android.inputmethod.keyboard.internal.ScrollKeyboardView;
 import com.android.inputmethod.keyboard.internal.ScrollViewWithNotifier;
 import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.SubtypeSwitcher;
+import com.android.inputmethod.latin.settings.Settings;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 import com.android.inputmethod.latin.utils.ResourceUtils;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
 import java.util.HashMap;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * View class to implement Emoji keyboards.
@@ -60,51 +71,287 @@
 public final class EmojiKeyboardView extends LinearLayout implements OnTabChangeListener,
         ViewPager.OnPageChangeListener, View.OnClickListener,
         ScrollKeyboardView.OnKeyClickListener {
+    private static final String TAG = EmojiKeyboardView.class.getSimpleName();
     private final int mKeyBackgroundId;
+    private final int mEmojiFunctionalKeyBackgroundId;
+    private final KeyboardLayoutSet mLayoutSet;
     private final ColorStateList mTabLabelColor;
-    private final EmojiKeyboardAdapter mEmojiKeyboardAdapter;
+    private EmojiKeyboardAdapter mEmojiKeyboardAdapter;
 
     private TabHost mTabHost;
     private ViewPager mEmojiPager;
 
     private KeyboardActionListener mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER;
 
-    private int mCurrentCategory = CATEGORY_UNSPECIFIED;
-    private static final int CATEGORY_UNSPECIFIED = -1;
-    private static final int CATEGORY_RECENTS = 0;
-    private static final int CATEGORY_PEOPLE = 1;
-    private static final int CATEGORY_OBJECTS = 2;
-    private static final int CATEGORY_NATURE = 3;
-    private static final int CATEGORY_PLACES = 4;
-    private static final int CATEGORY_SYMBOLS = 5;
-    private static final int CATEGORY_EMOTICONS = 6;
-    private static final HashMap<String, Integer> sCategoryNameToIdMap =
-            CollectionUtils.newHashMap();
-    private static final String[] sCategoryName = {
-        "recents", "people", "objects", "nature", "places", "symbols", "emoticons"
-    };
-    private static final int[] sCategoryIcon = new int[] {
-        R.drawable.ic_emoji_recent_light,
-        R.drawable.ic_emoji_people_light,
-        R.drawable.ic_emoji_objects_light,
-        R.drawable.ic_emoji_nature_light,
-        R.drawable.ic_emoji_places_light,
-        R.drawable.ic_emoji_symbols_light,
-        0
-    };
-    private static final String[] sCategoryLabel = {
-        null, null, null, null, null, null,
-        ":-)"
-    };
-    private static final int[] sCategoryElementId = {
-        KeyboardId.ELEMENT_EMOJI_RECENTS,
-        KeyboardId.ELEMENT_EMOJI_CATEGORY1,
-        KeyboardId.ELEMENT_EMOJI_CATEGORY2,
-        KeyboardId.ELEMENT_EMOJI_CATEGORY3,
-        KeyboardId.ELEMENT_EMOJI_CATEGORY4,
-        KeyboardId.ELEMENT_EMOJI_CATEGORY5,
-        KeyboardId.ELEMENT_EMOJI_CATEGORY6,
-    };
+    private static final int CATEGORY_ID_UNSPECIFIED = -1;
+    public static final int CATEGORY_ID_RECENTS = 0;
+    public static final int CATEGORY_ID_PEOPLE = 1;
+    public static final int CATEGORY_ID_OBJECTS = 2;
+    public static final int CATEGORY_ID_NATURE = 3;
+    public static final int CATEGORY_ID_PLACES = 4;
+    public static final int CATEGORY_ID_SYMBOLS = 5;
+    public static final int CATEGORY_ID_EMOTICONS = 6;
+
+    private static class CategoryProperties {
+        public int mCategoryId;
+        public int mPageCount;
+        public CategoryProperties(final int categoryId, final int pageCount) {
+            mCategoryId = categoryId;
+            mPageCount = pageCount;
+        }
+    }
+
+    private static class EmojiCategory {
+        private static final String[] sCategoryName = {
+                "recents",
+                "people",
+                "objects",
+                "nature",
+                "places",
+                "symbols",
+                "emoticons" };
+        private static final int[] sCategoryIcon = new int[] {
+                R.drawable.ic_emoji_recent_light,
+                R.drawable.ic_emoji_people_light,
+                R.drawable.ic_emoji_objects_light,
+                R.drawable.ic_emoji_nature_light,
+                R.drawable.ic_emoji_places_light,
+                R.drawable.ic_emoji_symbols_light,
+                0 };
+        private static final String[] sCategoryLabel =
+                { null, null, null, null, null, null, ":-)" };
+        private static final int[] sCategoryElementId = {
+                KeyboardId.ELEMENT_EMOJI_RECENTS,
+                KeyboardId.ELEMENT_EMOJI_CATEGORY1,
+                KeyboardId.ELEMENT_EMOJI_CATEGORY2,
+                KeyboardId.ELEMENT_EMOJI_CATEGORY3,
+                KeyboardId.ELEMENT_EMOJI_CATEGORY4,
+                KeyboardId.ELEMENT_EMOJI_CATEGORY5,
+                KeyboardId.ELEMENT_EMOJI_CATEGORY6 };
+        private final SharedPreferences mPrefs;
+        private final int mMaxPageKeyCount;
+        private final KeyboardLayoutSet mLayoutSet;
+        private final HashMap<String, Integer> mCategoryNameToIdMap = CollectionUtils.newHashMap();
+        private final ArrayList<CategoryProperties> mShownCategories =
+                CollectionUtils.newArrayList();
+        private final ConcurrentHashMap<Long, DynamicGridKeyboard>
+                mCategoryKeyboardMap = new ConcurrentHashMap<Long, DynamicGridKeyboard>();
+
+        private int mCurrentCategoryId = CATEGORY_ID_UNSPECIFIED;
+        private int mCurrentCategoryPageId = 0;
+
+        public EmojiCategory(final SharedPreferences prefs, final Resources res,
+                final KeyboardLayoutSet layoutSet) {
+            mPrefs = prefs;
+            mMaxPageKeyCount = res.getInteger(R.integer.emoji_keyboard_max_key_count);
+            mLayoutSet = layoutSet;
+            for (int i = 0; i < sCategoryName.length; ++i) {
+                mCategoryNameToIdMap.put(sCategoryName[i], i);
+            }
+            addShownCategoryId(CATEGORY_ID_RECENTS);
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+                addShownCategoryId(CATEGORY_ID_PEOPLE);
+                addShownCategoryId(CATEGORY_ID_OBJECTS);
+                addShownCategoryId(CATEGORY_ID_NATURE);
+                addShownCategoryId(CATEGORY_ID_PLACES);
+                mCurrentCategoryId = CATEGORY_ID_PEOPLE;
+            } else {
+                mCurrentCategoryId = CATEGORY_ID_SYMBOLS;
+            }
+            addShownCategoryId(CATEGORY_ID_SYMBOLS);
+            addShownCategoryId(CATEGORY_ID_EMOTICONS);
+            getKeyboard(CATEGORY_ID_RECENTS, 0 /* cagetoryPageId */)
+                    .loadRecentKeys(mCategoryKeyboardMap.values());
+        }
+
+        private void addShownCategoryId(int categoryId) {
+            // Load a keyboard of categoryId
+            getKeyboard(categoryId, 0 /* cagetoryPageId */);
+            final CategoryProperties properties =
+                    new CategoryProperties(categoryId, getCategoryPageCount(categoryId));
+            mShownCategories.add(properties);
+        }
+
+        public String getCategoryName(int categoryId, int categoryPageId) {
+            return sCategoryName[categoryId] + "-" + categoryPageId;
+        }
+
+        public int getCategoryId(String name) {
+            final String[] strings = name.split("-");
+            return mCategoryNameToIdMap.get(strings[0]);
+        }
+
+        public int getCategoryIcon(int categoryId) {
+            return sCategoryIcon[categoryId];
+        }
+
+        public String getCategoryLabel(int categoryId) {
+            return sCategoryLabel[categoryId];
+        }
+
+        public ArrayList<CategoryProperties> getShownCategories() {
+            return mShownCategories;
+        }
+
+        public int getCurrentCategoryId() {
+            return mCurrentCategoryId;
+        }
+
+        public void setCurrentCategoryId(int categoryId) {
+            mCurrentCategoryId = categoryId;
+        }
+
+        public void setCurrentCategoryPageId(int id) {
+            mCurrentCategoryPageId = id;
+        }
+
+        public void saveLastTypedCategoryPage() {
+            Settings.writeEmojiCategoryLastTypedId(
+                    mPrefs, mCurrentCategoryId, mCurrentCategoryPageId);
+        }
+
+        public boolean isInRecentTab() {
+            return mCurrentCategoryId == CATEGORY_ID_RECENTS;
+        }
+
+        public int getTabIdFromCategoryId(int categoryId) {
+            for (int i = 0; i < mShownCategories.size(); ++i) {
+                if (mShownCategories.get(i).mCategoryId == categoryId) {
+                    return i;
+                }
+            }
+            Log.w(TAG, "categoryId not found: " + categoryId);
+            return 0;
+        }
+
+        // Returns the view pager's page position for the categoryId
+        public int getPageIdFromCategoryId(int categoryId) {
+            final int lastSavedCategoryPageId =
+                    Settings.readEmojiCategoryLastTypedId(mPrefs, categoryId);
+            int sum = 0;
+            for (int i = 0; i < mShownCategories.size(); ++i) {
+                final CategoryProperties props = mShownCategories.get(i);
+                if (props.mCategoryId == categoryId) {
+                    return sum + lastSavedCategoryPageId;
+                }
+                sum += props.mPageCount;
+            }
+            Log.w(TAG, "categoryId not found: " + categoryId);
+            return 0;
+        }
+
+        public int getRecentTabId() {
+            return getTabIdFromCategoryId(CATEGORY_ID_RECENTS);
+        }
+
+        private int getCategoryPageCount(int categoryId) {
+            final Keyboard keyboard = mLayoutSet.getKeyboard(sCategoryElementId[categoryId]);
+            return (keyboard.getKeys().length - 1) / mMaxPageKeyCount + 1;
+        }
+
+        // Returns a pair of the category id and the category page id from the view pager's page
+        // position. The category page id is numbered in each category. And the view page position
+        // is the position of the current shown page in the view pager which contains all pages of
+        // all categories.
+        public Pair<Integer, Integer> getCategoryIdAndPageIdFromPagePosition(int position) {
+            int sum = 0;
+            for (CategoryProperties properties : mShownCategories) {
+                final int temp = sum;
+                sum += properties.mPageCount;
+                if (sum > position) {
+                    return new Pair<Integer, Integer>(properties.mCategoryId, position - temp);
+                }
+            }
+            return null;
+        }
+
+        // Returns a keyboard from the view pager's page position.
+        public DynamicGridKeyboard getKeyboardFromPagePosition(int position) {
+            final Pair<Integer, Integer> categoryAndId =
+                    getCategoryIdAndPageIdFromPagePosition(position);
+            if (categoryAndId != null) {
+                return getKeyboard(categoryAndId.first, categoryAndId.second);
+            }
+            return null;
+        }
+
+        public DynamicGridKeyboard getKeyboard(int categoryId, int id) {
+            synchronized(mCategoryKeyboardMap) {
+                final long key = (((long) categoryId) << Constants.MAX_INT_BIT_COUNT) | id;
+                final DynamicGridKeyboard kbd;
+                if (!mCategoryKeyboardMap.containsKey(key)) {
+                    if (categoryId != CATEGORY_ID_RECENTS) {
+                        final Keyboard keyboard =
+                                mLayoutSet.getKeyboard(sCategoryElementId[categoryId]);
+                        final Key[][] sortedKeys = sortKeys(keyboard.getKeys(), mMaxPageKeyCount);
+                        for (int i = 0; i < sortedKeys.length; ++i) {
+                            final DynamicGridKeyboard tempKbd = new DynamicGridKeyboard(mPrefs,
+                                    mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS),
+                                    mMaxPageKeyCount, categoryId, i /* categoryPageId */);
+                            for (Key emojiKey : sortedKeys[i]) {
+                                if (emojiKey == null) {
+                                    break;
+                                }
+                                tempKbd.addKeyLast(emojiKey);
+                            }
+                            mCategoryKeyboardMap.put((((long) categoryId)
+                                    << Constants.MAX_INT_BIT_COUNT) | i, tempKbd);
+                        }
+                        kbd = mCategoryKeyboardMap.get(key);
+                    } else {
+                        kbd = new DynamicGridKeyboard(mPrefs,
+                                mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS),
+                                mMaxPageKeyCount, categoryId, 0 /* categoryPageId */);
+                        mCategoryKeyboardMap.put(key, kbd);
+                    }
+                } else {
+                    kbd = mCategoryKeyboardMap.get(key);
+                }
+                return kbd;
+            }
+        }
+
+        public int getTotalPageCountOfAllCategories() {
+            int sum = 0;
+            for (CategoryProperties properties : mShownCategories) {
+                sum += properties.mPageCount;
+            }
+            return sum;
+        }
+
+        private Key[][] sortKeys(Key[] inKeys, int maxPageCount) {
+            Key[] keys = Arrays.copyOf(inKeys, inKeys.length);
+            Arrays.sort(keys, 0, keys.length, new Comparator<Key>() {
+                @Override
+                public int compare(Key lhs, Key rhs) {
+                    final Rect lHitBox = lhs.getHitBox();
+                    final Rect rHitBox = rhs.getHitBox();
+                    if (lHitBox.top < rHitBox.top) {
+                        return -1;
+                    } else if (lHitBox.top > rHitBox.top) {
+                        return 1;
+                    }
+                    if (lHitBox.left < rHitBox.left) {
+                        return -1;
+                    } else if (lHitBox.left > rHitBox.left) {
+                        return 1;
+                    }
+                    if (lhs.getCode() == rhs.getCode()) {
+                        return 0;
+                    }
+                    return lhs.getCode() < rhs.getCode() ? -1 : 1;
+                }
+            });
+            final int pageCount = (keys.length - 1) / maxPageCount + 1;
+            final Key[][] retval = new Key[pageCount][maxPageCount];
+            for (int i = 0; i < keys.length; ++i) {
+                retval[i / maxPageCount][i % maxPageCount] = keys[i];
+            }
+            return retval;
+        }
+    }
+
+    private final EmojiCategory mEmojiCategory;
 
     public EmojiKeyboardView(final Context context, final AttributeSet attrs) {
         this(context, attrs, R.attr.emojiKeyboardViewStyle);
@@ -116,6 +363,8 @@
                 R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
         mKeyBackgroundId = keyboardViewAttr.getResourceId(
                 R.styleable.KeyboardView_keyBackground, 0);
+        mEmojiFunctionalKeyBackgroundId = keyboardViewAttr.getResourceId(
+                R.styleable.KeyboardView_keyBackgroundEmojiFunctional, 0);
         keyboardViewAttr.recycle();
         final TypedArray emojiKeyboardViewAttr = context.obtainStyledAttributes(attrs,
                 R.styleable.EmojiKeyboardView, defStyle, R.style.EmojiKeyboardView);
@@ -125,15 +374,14 @@
         final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
                 context, null /* editorInfo */);
         final Resources res = context.getResources();
+        final EmojiLayoutParams emojiLp = new EmojiLayoutParams(res);
         builder.setSubtype(SubtypeSwitcher.getInstance().getEmojiSubtype());
-        // TODO: Make Keyboard height variable.
         builder.setKeyboardGeometry(ResourceUtils.getDefaultKeyboardWidth(res),
-                (int)(ResourceUtils.getDefaultKeyboardHeight(res)
-                        - res.getDimension(R.dimen.suggestions_strip_height)));
+                emojiLp.mEmojiKeyboardHeight);
         builder.setOptions(false, false, false /* lanuageSwitchKeyEnabled */);
-        final KeyboardLayoutSet layoutSet = builder.build();
-        mEmojiKeyboardAdapter = new EmojiKeyboardAdapter(layoutSet, this);
-        // TODO: Save/restore recent keys from/to preferences.
+        mLayoutSet = builder.build();
+        mEmojiCategory = new EmojiCategory(PreferenceManager.getDefaultSharedPreferences(context),
+                context.getResources(), builder.build());
     }
 
     @Override
@@ -149,23 +397,21 @@
         setMeasuredDimension(width, height);
     }
 
-    private void addTab(final TabHost host, final int category) {
-        final String tabId = sCategoryName[category];
-        sCategoryNameToIdMap.put(tabId, category);
+    private void addTab(final TabHost host, final int categoryId) {
+        final String tabId = mEmojiCategory.getCategoryName(categoryId, 0 /* categoryPageId */);
         final TabHost.TabSpec tspec = host.newTabSpec(tabId);
         tspec.setContent(R.id.emoji_keyboard_dummy);
-        if (sCategoryIcon[category] != 0) {
+        if (mEmojiCategory.getCategoryIcon(categoryId) != 0) {
             final ImageView iconView = (ImageView)LayoutInflater.from(getContext()).inflate(
                     R.layout.emoji_keyboard_tab_icon, null);
-            iconView.setImageResource(sCategoryIcon[category]);
+            iconView.setImageResource(mEmojiCategory.getCategoryIcon(categoryId));
             tspec.setIndicator(iconView);
         }
-        if (sCategoryLabel[category] != null) {
+        if (mEmojiCategory.getCategoryLabel(categoryId) != null) {
             final TextView textView = (TextView)LayoutInflater.from(getContext()).inflate(
                     R.layout.emoji_keyboard_tab_label, null);
-            textView.setText(sCategoryLabel[category]);
+            textView.setText(mEmojiCategory.getCategoryLabel(categoryId));
             textView.setTextColor(mTabLabelColor);
-            textView.setBackgroundResource(mKeyBackgroundId);
             tspec.setIndicator(textView);
         }
         host.addTab(tspec);
@@ -175,55 +421,60 @@
     protected void onFinishInflate() {
         mTabHost = (TabHost)findViewById(R.id.emoji_category_tabhost);
         mTabHost.setup();
-        addTab(mTabHost, CATEGORY_RECENTS);
-        addTab(mTabHost, CATEGORY_PEOPLE);
-        addTab(mTabHost, CATEGORY_OBJECTS);
-        addTab(mTabHost, CATEGORY_NATURE);
-        addTab(mTabHost, CATEGORY_PLACES);
-        addTab(mTabHost, CATEGORY_SYMBOLS);
-        addTab(mTabHost, CATEGORY_EMOTICONS);
+        for (final CategoryProperties properties : mEmojiCategory.getShownCategories()) {
+            addTab(mTabHost, properties.mCategoryId);
+        }
         mTabHost.setOnTabChangedListener(this);
         mTabHost.getTabWidget().setStripEnabled(true);
 
+        mEmojiKeyboardAdapter = new EmojiKeyboardAdapter(mEmojiCategory, mLayoutSet, this);
+
         mEmojiPager = (ViewPager)findViewById(R.id.emoji_keyboard_pager);
         mEmojiPager.setAdapter(mEmojiKeyboardAdapter);
         mEmojiPager.setOnPageChangeListener(this);
         mEmojiPager.setOffscreenPageLimit(0);
-        final ViewGroup.LayoutParams lp = mEmojiPager.getLayoutParams();
         final Resources res = getResources();
-        lp.height = ResourceUtils.getDefaultKeyboardHeight(res)
-                - res.getDimensionPixelSize(R.dimen.suggestions_strip_height);
-        mEmojiPager.setLayoutParams(lp);
+        final EmojiLayoutParams emojiLp = new EmojiLayoutParams(res);
+        emojiLp.setPagerProps(mEmojiPager);
 
-        // TODO: Record current category.
-        final int category = CATEGORY_PEOPLE;
-        setCurrentCategory(category, true /* force */);
+        setCurrentCategoryId(mEmojiCategory.getCurrentCategoryId(), true /* force */);
+
+        final LinearLayout actionBar = (LinearLayout)findViewById(R.id.emoji_action_bar);
+        emojiLp.setActionBarProps(actionBar);
 
         // TODO: Implement auto repeat, using View.OnTouchListener?
-        final View deleteKey = findViewById(R.id.emoji_keyboard_delete);
-        deleteKey.setBackgroundResource(mKeyBackgroundId);
+        final ImageView deleteKey = (ImageView)findViewById(R.id.emoji_keyboard_delete);
+        deleteKey.setBackgroundResource(mEmojiFunctionalKeyBackgroundId);
         deleteKey.setTag(Constants.CODE_DELETE);
         deleteKey.setOnClickListener(this);
-        final View alphabetKey = findViewById(R.id.emoji_keyboard_alphabet);
-        alphabetKey.setBackgroundResource(mKeyBackgroundId);
+        final ImageView alphabetKey = (ImageView)findViewById(R.id.emoji_keyboard_alphabet);
+        alphabetKey.setBackgroundResource(mEmojiFunctionalKeyBackgroundId);
         alphabetKey.setTag(Constants.CODE_SWITCH_ALPHA_SYMBOL);
         alphabetKey.setOnClickListener(this);
-        final View sendKey = findViewById(R.id.emoji_keyboard_send);
-        sendKey.setBackgroundResource(mKeyBackgroundId);
+        final ImageView spaceKey = (ImageView)findViewById(R.id.emoji_keyboard_space);
+        spaceKey.setBackgroundResource(mKeyBackgroundId);
+        spaceKey.setTag(Constants.CODE_SPACE);
+        spaceKey.setOnClickListener(this);
+        emojiLp.setKeyProps(spaceKey);
+        final ImageView sendKey = (ImageView)findViewById(R.id.emoji_keyboard_send);
+        sendKey.setBackgroundResource(mEmojiFunctionalKeyBackgroundId);
         sendKey.setTag(Constants.CODE_ENTER);
         sendKey.setOnClickListener(this);
     }
 
     @Override
     public void onTabChanged(final String tabId) {
-        final int category = sCategoryNameToIdMap.get(tabId);
-        setCurrentCategory(category, false /* force */);
+        final int categoryId = mEmojiCategory.getCategoryId(tabId);
+        setCurrentCategoryId(categoryId, false /* force */);
     }
 
 
     @Override
     public void onPageSelected(final int position) {
-        setCurrentCategory(position, false /* force */);
+        final Pair<Integer, Integer> newPos =
+                mEmojiCategory.getCategoryIdAndPageIdFromPagePosition(position);
+        setCurrentCategoryId(newPos.first /* categoryId */, false /* force */);
+        mEmojiCategory.setCurrentCategoryPageId(newPos.second /* categoryPageId */);
     }
 
     @Override
@@ -254,7 +505,11 @@
 
     @Override
     public void onKeyClick(final Key key) {
-        mEmojiKeyboardAdapter.addRecentKey(key);
+        // TODO: Save emoticons to recents
+        if (mEmojiCategory.getCurrentCategoryId() != CATEGORY_ID_EMOTICONS) {
+            mEmojiKeyboardAdapter.addRecentKey(key);
+        }
+        mEmojiCategory.saveLastTypedCategoryPage();
         final int code = key.getCode();
         if (code == Constants.CODE_OUTPUT_TEXT) {
             mKeyboardActionListener.onTextInput(key.getOutputText());
@@ -271,43 +526,46 @@
         mKeyboardActionListener = listener;
     }
 
-    private void setCurrentCategory(final int category, final boolean force) {
-        if (mCurrentCategory == category && !force) {
+    private void setCurrentCategoryId(final int categoryId, final boolean force) {
+        if (mEmojiCategory.getCurrentCategoryId() == categoryId && !force) {
             return;
         }
 
-        mCurrentCategory = category;
-        if (force || mEmojiPager.getCurrentItem() != category) {
-            mEmojiPager.setCurrentItem(category, true /* smoothScroll */);
+        mEmojiCategory.setCurrentCategoryId(categoryId);
+        final int newTabId = mEmojiCategory.getTabIdFromCategoryId(categoryId);
+        final int newCategoryPageId = mEmojiCategory.getPageIdFromCategoryId(categoryId);
+        if (force || mEmojiCategory.getCategoryIdAndPageIdFromPagePosition(
+                mEmojiPager.getCurrentItem()).first != categoryId) {
+            mEmojiPager.setCurrentItem(newCategoryPageId, false /* smoothScroll */);
         }
-        if (force || mTabHost.getCurrentTab() != category) {
-            mTabHost.setCurrentTab(category);
+        if (force || mTabHost.getCurrentTab() != newTabId) {
+            mTabHost.setCurrentTab(newTabId);
         }
-        // TODO: Record current category
     }
 
     private static class EmojiKeyboardAdapter extends PagerAdapter {
         private final ScrollKeyboardView.OnKeyClickListener mListener;
-        private final KeyboardLayoutSet mLayoutSet;
-        private final RecentsKeyboard mRecentsKeyboard;
+        private final DynamicGridKeyboard mRecentsKeyboard;
         private final SparseArray<ScrollKeyboardView> mActiveKeyboardView =
                 CollectionUtils.newSparseArray();
-        private int mActivePosition = CATEGORY_UNSPECIFIED;
+        private final EmojiCategory mEmojiCategory;
+        private int mActivePosition = 0;
 
-        public EmojiKeyboardAdapter(final KeyboardLayoutSet layoutSet,
+        public EmojiKeyboardAdapter(final EmojiCategory emojiCategory,
+                final KeyboardLayoutSet layoutSet,
                 final ScrollKeyboardView.OnKeyClickListener listener) {
+            mEmojiCategory = emojiCategory;
             mListener = listener;
-            mLayoutSet = layoutSet;
-            mRecentsKeyboard = new RecentsKeyboard(
-                    layoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS));
+            mRecentsKeyboard = mEmojiCategory.getKeyboard(CATEGORY_ID_RECENTS, 0);
         }
 
         public void addRecentKey(final Key key) {
-            if (mActivePosition == CATEGORY_RECENTS) {
+            if (mEmojiCategory.isInRecentTab()) {
                 return;
             }
-            mRecentsKeyboard.addRecentKey(key);
-            final KeyboardView recentKeyboardView = mActiveKeyboardView.get(CATEGORY_RECENTS);
+            mRecentsKeyboard.addKeyFirst(key);
+            final KeyboardView recentKeyboardView =
+                    mActiveKeyboardView.get(mEmojiCategory.getRecentTabId());
             if (recentKeyboardView != null) {
                 recentKeyboardView.invalidateAllKeys();
             }
@@ -315,7 +573,7 @@
 
         @Override
         public int getCount() {
-            return sCategoryName.length;
+            return mEmojiCategory.getTotalPageCountOfAllCategories();
         }
 
         @Override
@@ -333,9 +591,8 @@
 
         @Override
         public Object instantiateItem(final ViewGroup container, final int position) {
-            final int elementId = sCategoryElementId[position];
-            final Keyboard keyboard = (elementId == KeyboardId.ELEMENT_EMOJI_RECENTS)
-                    ? mRecentsKeyboard : mLayoutSet.getKeyboard(elementId);
+            final Keyboard keyboard =
+                    mEmojiCategory.getKeyboardFromPagePosition(position);
             final LayoutInflater inflater = LayoutInflater.from(container.getContext());
             final View view = inflater.inflate(
                     R.layout.emoji_keyboard_page, container, false /* attachToRoot */);
diff --git a/java/src/com/android/inputmethod/keyboard/EmojiLayoutParams.java b/java/src/com/android/inputmethod/keyboard/EmojiLayoutParams.java
new file mode 100644
index 0000000..5570d59
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/EmojiLayoutParams.java
@@ -0,0 +1,80 @@
+/*
+ * 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.
+ */
+
+package com.android.inputmethod.keyboard;
+
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.utils.ResourceUtils;
+
+import android.content.res.Resources;
+import android.support.v4.view.ViewPager;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+public class EmojiLayoutParams {
+    private static final int DEFAULT_KEYBOARD_ROWS = 4;
+
+    public final int mEmojiPagerHeight;
+    private final int mEmojiPagerBottomMargin;
+    public final int mEmojiKeyboardHeight;
+    public final int mEmojiActionBarHeight;
+    public final int mKeyVerticalGap;
+    private final int mKeyHorizontalGap;
+    private final int mBottomPadding;
+    private final int mTopPadding;
+
+    public EmojiLayoutParams(Resources res) {
+        final int defaultKeyboardHeight = ResourceUtils.getDefaultKeyboardHeight(res);
+        final int defaultKeyboardWidth = ResourceUtils.getDefaultKeyboardWidth(res);
+        mKeyVerticalGap = (int) res.getFraction(R.fraction.key_bottom_gap_ics,
+                (int) defaultKeyboardHeight, (int) defaultKeyboardHeight);
+        mBottomPadding = (int) res.getFraction(R.fraction.keyboard_bottom_padding_ics,
+                (int) defaultKeyboardHeight, (int) defaultKeyboardHeight);
+        mTopPadding = (int) res.getFraction(R.fraction.keyboard_top_padding_ics,
+                (int) defaultKeyboardHeight, (int) defaultKeyboardHeight);
+        mKeyHorizontalGap = (int) (res.getFraction(R.fraction.key_horizontal_gap_ics,
+                defaultKeyboardWidth, defaultKeyboardWidth));
+        final int baseheight = defaultKeyboardHeight - mBottomPadding - mTopPadding
+                + mKeyVerticalGap;
+        mEmojiActionBarHeight = ((int) baseheight) / DEFAULT_KEYBOARD_ROWS
+                - (mKeyVerticalGap - mBottomPadding) / 2;
+        mEmojiPagerHeight = defaultKeyboardHeight - mEmojiActionBarHeight;
+        mEmojiPagerBottomMargin = mKeyVerticalGap / 2;
+        mEmojiKeyboardHeight = mEmojiPagerHeight - mEmojiPagerBottomMargin - 1;
+    }
+
+    public void setPagerProps(ViewPager vp) {
+        final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) vp.getLayoutParams();
+        lp.height = mEmojiPagerHeight - mEmojiPagerBottomMargin;
+        lp.bottomMargin = mEmojiPagerBottomMargin;
+        vp.setLayoutParams(lp);
+    }
+
+    public void setActionBarProps(LinearLayout ll) {
+        final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) ll.getLayoutParams();
+        lp.height = mEmojiActionBarHeight;
+        lp.topMargin = 0;
+        lp.bottomMargin = mBottomPadding;
+        ll.setLayoutParams(lp);
+    }
+
+    public void setKeyProps(ImageView ib) {
+        final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) ib.getLayoutParams();
+        lp.leftMargin = mKeyHorizontalGap / 2;
+        lp.rightMargin = mKeyHorizontalGap / 2;
+        ib.setLayoutParams(lp);
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index d128d35..cc1ffd1 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -314,15 +314,19 @@
         mState.onCodeInput(code, mLatinIME.getCurrentAutoCapsState());
     }
 
+    public boolean isShowingEmojiKeyboard() {
+        return mEmojiKeyboardView.getVisibility() == View.VISIBLE;
+    }
+
     public boolean isShowingMoreKeysPanel() {
-        if (mEmojiKeyboardView.getVisibility() == View.VISIBLE) {
+        if (isShowingEmojiKeyboard()) {
             return false;
         }
         return mKeyboardView.isShowingMoreKeysPanel();
     }
 
     public View getVisibleKeyboardView() {
-        if (mEmojiKeyboardView.getVisibility() == View.VISIBLE) {
+        if (isShowingEmojiKeyboard()) {
             return mEmojiKeyboardView;
         }
         return mKeyboardView;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java b/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java
new file mode 100644
index 0000000..f203eb7
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java
@@ -0,0 +1,201 @@
+/*
+ * 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.
+ */
+
+package com.android.inputmethod.keyboard.internal;
+
+import android.content.SharedPreferences;
+import android.text.TextUtils;
+
+import com.android.inputmethod.keyboard.EmojiKeyboardView;
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.settings.Settings;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+
+import java.util.ArrayDeque;
+import java.util.Collection;
+
+/**
+ * This is a Keyboard class where you can add keys dynamically shown in a grid layout
+ */
+public class DynamicGridKeyboard extends Keyboard {
+    private static final int TEMPLATE_KEY_CODE_0 = 0x30;
+    private static final int TEMPLATE_KEY_CODE_1 = 0x31;
+    // Recent codes are saved as an integer array, so we use comma as a separater.
+    private static final String RECENT_KEY_SEPARATOR = Constants.STRING_COMMA;
+
+    private final SharedPreferences mPrefs;
+    private final int mLeftPadding;
+    private final int mHorizontalStep;
+    private final int mVerticalStep;
+    private final int mColumnsNum;
+    private final int mMaxKeyCount;
+    private final boolean mIsRecents;
+    private final ArrayDeque<GridKey> mGridKeys = CollectionUtils.newArrayDeque();
+
+    private Key[] mCachedGridKeys;
+
+    public DynamicGridKeyboard(final SharedPreferences prefs, final Keyboard templateKeyboard,
+            final int maxKeyCount, final int categoryId, final int categoryPageId) {
+        super(templateKeyboard);
+        final Key key0 = getTemplateKey(TEMPLATE_KEY_CODE_0);
+        final Key key1 = getTemplateKey(TEMPLATE_KEY_CODE_1);
+        mLeftPadding = key0.getX();
+        mHorizontalStep = Math.abs(key1.getX() - key0.getX());
+        mVerticalStep = key0.getHeight() + mVerticalGap;
+        mColumnsNum = mBaseWidth / mHorizontalStep;
+        mMaxKeyCount = maxKeyCount;
+        mIsRecents = categoryId == EmojiKeyboardView.CATEGORY_ID_RECENTS;
+        mPrefs = prefs;
+    }
+
+    private Key getTemplateKey(final int code) {
+        for (final Key key : super.getKeys()) {
+            if (key.getCode() == code) {
+                return key;
+            }
+        }
+        throw new RuntimeException("Can't find template key: code=" + code);
+    }
+
+    public void addKeyFirst(final Key usedKey) {
+        addKey(usedKey, true);
+        if (mIsRecents) {
+            saveRecentKeys();
+        }
+    }
+
+    public void addKeyLast(final Key usedKey) {
+        addKey(usedKey, false);
+    }
+
+    private void addKey(final Key usedKey, final boolean addFirst) {
+        synchronized (mGridKeys) {
+            mCachedGridKeys = null;
+            final GridKey key = new GridKey(usedKey);
+            while (mGridKeys.remove(key)) {
+                // Remove duplicate keys.
+            }
+            if (addFirst) {
+                mGridKeys.addFirst(key);
+            } else {
+                mGridKeys.addLast(key);
+            }
+            while (mGridKeys.size() > mMaxKeyCount) {
+                mGridKeys.removeLast();
+            }
+            int index = 0;
+            for (final GridKey gridKey : mGridKeys) {
+                final int keyX = getKeyX(index);
+                final int keyY = getKeyY(index);
+                gridKey.updateCorrdinates(keyX, keyY);
+                index++;
+            }
+        }
+    }
+
+    private void saveRecentKeys() {
+        final StringBuilder sb = new StringBuilder();
+        for (final Key key : mGridKeys) {
+            sb.append(key.getCode()).append(RECENT_KEY_SEPARATOR);
+        }
+        Settings.writeEmojiRecentKeys(mPrefs, sb.toString());
+    }
+
+    public void loadRecentKeys(Collection<DynamicGridKeyboard> keyboards) {
+        final String str = Settings.readEmojiRecentKeys(mPrefs);
+        for (String s : str.split(RECENT_KEY_SEPARATOR)) {
+            if (TextUtils.isEmpty(s)) {
+                continue;
+            }
+            final int code = Integer.valueOf(s);
+            for (DynamicGridKeyboard kbd : keyboards) {
+                final Key key = kbd.getKey(code);
+                if (key != null) {
+                    addKeyLast(key);
+                    break;
+                }
+            }
+        }
+    }
+
+    private int getKeyX(final int index) {
+        final int column = index % mColumnsNum;
+        return column * mHorizontalStep + mLeftPadding;
+    }
+
+    private int getKeyY(final int index) {
+        final int row = index / mColumnsNum;
+        return row * mVerticalStep + mTopPadding;
+    }
+
+    @Override
+    public Key[] getKeys() {
+        synchronized (mGridKeys) {
+            if (mCachedGridKeys != null) {
+                return mCachedGridKeys;
+            }
+            mCachedGridKeys = mGridKeys.toArray(new Key[mGridKeys.size()]);
+            return mCachedGridKeys;
+        }
+    }
+
+    @Override
+    public Key[] getNearestKeys(final int x, final int y) {
+        // TODO: Calculate the nearest key index in mGridKeys from x and y.
+        return getKeys();
+    }
+
+    static final class GridKey extends Key {
+        private int mCurrentX;
+        private int mCurrentY;
+
+        public GridKey(final Key originalKey) {
+            super(originalKey);
+        }
+
+        public void updateCorrdinates(final int x, final int y) {
+            mCurrentX = x;
+            mCurrentY = y;
+            getHitBox().offsetTo(x, y);
+        }
+
+        @Override
+        public int getX() {
+            return mCurrentX;
+        }
+
+        @Override
+        public int getY() {
+            return mCurrentY;
+        }
+
+        @Override
+        public boolean equals(final Object o) {
+            if (!(o instanceof Key)) return false;
+            final Key key = (Key)o;
+            if (getCode() != key.getCode()) return false;
+            if (!TextUtils.equals(getLabel(), key.getLabel())) return false;
+            return TextUtils.equals(getOutputText(), key.getOutputText());
+        }
+
+        @Override
+        public String toString() {
+            return "GridKey: " + super.toString();
+        }
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
index 7008b06..a72595f 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
@@ -383,7 +383,7 @@
         // Label for "switch to more symbol" modifier key.  Must be short to fit on key!
         /* 124 */ "= \\ <",
         // Label for "switch to more symbol" modifier key on tablets.  Must be short to fit on key!
-        /* 125 */ "~ [ {",
+        /* 125 */ "~ [ <",
         // Label for "Tab" key.  Must be short to fit on key!
         /* 126 */ "Tab",
         // Label for "switch to phone numeric" key.  Must be short to fit on key!
@@ -2056,6 +2056,25 @@
         /* 45 */ "\u0410\u0411\u0412",
     };
 
+    /* Language lo: Lao */
+    private static final String[] LANGUAGE_lo = {
+        /* 0~ */
+        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+        /* ~44 */
+        // Label for "switch to alphabetic" key.
+        // U+0E81: "ກ" LAO LETTER KO
+        // U+0E82: "ຂ" LAO LETTER KHO SUNG
+        // U+0E84: "ຄ" LAO LETTER KHO TAM
+        /* 45 */ "\u0E81\u0E82\u0E84",
+        /* 46~ */
+        null, null, null, null, null,
+        /* ~50 */
+        // U+20AD: "₭" KIP SIGN
+        /* 51 */ "\u20AD",
+    };
+
     /* Language lt: Lithuanian */
     private static final String[] LANGUAGE_lt = {
         // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK
@@ -2347,6 +2366,63 @@
         /* 47 */ "!text/double_9qm_rqm",
     };
 
+    /* Language ne: Nepali */
+    private static final String[] LANGUAGE_ne = {
+        /* 0~ */
+        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+        /* ~44 */
+        // Label for "switch to alphabetic" key.
+        // U+0915: "क" DEVANAGARI LETTER KA
+        // U+0916: "ख" DEVANAGARI LETTER KHA
+        // U+0917: "ग" DEVANAGARI LETTER GA
+        /* 45 */ "\u0915\u0916\u0917",
+        /* 46~ */
+        null, null, null, null, null,
+        /* ~50 */
+        // U+0930/U+0941/U+002E "रु." NEPALESE RUPEE SIGN
+        /* 51 */ "\u0930\u0941.",
+        /* 52~ */
+        null, null, null, null, null, null, null, null, null, null, null,
+        /* ~62 */
+        // U+0967: "१" DEVANAGARI DIGIT ONE
+        /* 63 */ "\u0967",
+        // U+0968: "२" DEVANAGARI DIGIT TWO
+        /* 64 */ "\u0968",
+        // U+0969: "३" DEVANAGARI DIGIT THREE
+        /* 65 */ "\u0969",
+        // U+096A: "४" DEVANAGARI DIGIT FOUR
+        /* 66 */ "\u096A",
+        // U+096B: "५" DEVANAGARI DIGIT FIVE
+        /* 67 */ "\u096B",
+        // U+096C: "६" DEVANAGARI DIGIT SIX
+        /* 68 */ "\u096C",
+        // U+096D: "७" DEVANAGARI DIGIT SEVEN
+        /* 69 */ "\u096D",
+        // U+096E: "८" DEVANAGARI DIGIT EIGHT
+        /* 70 */ "\u096E",
+        // U+096F: "९" DEVANAGARI DIGIT NINE
+        /* 71 */ "\u096F",
+        // U+0966: "०" DEVANAGARI DIGIT ZERO
+        /* 72 */ "\u0966",
+        // Label for "switch to symbols" key.
+        /* 73 */ "?\u0967\u0968\u0969",
+        // Label for "switch to symbols with microphone" key. This string shouldn't include the "mic"
+        // part because it'll be appended by the code.
+        /* 74 */ "\u0967\u0968\u0969",
+        /* 75 */ "1",
+        /* 76 */ "2",
+        /* 77 */ "3",
+        /* 78 */ "4",
+        /* 79 */ "5",
+        /* 80 */ "6",
+        /* 81 */ "7",
+        /* 82 */ "8",
+        /* 83 */ "9",
+        /* 84 */ "0",
+    };
+
     /* Language nl: Dutch */
     private static final String[] LANGUAGE_nl = {
         // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
@@ -3332,11 +3408,13 @@
         "ka", LANGUAGE_ka, /* Georgian */
         "kk", LANGUAGE_kk, /* Kazakh */
         "ky", LANGUAGE_ky, /* Kirghiz */
+        "lo", LANGUAGE_lo, /* Lao */
         "lt", LANGUAGE_lt, /* Lithuanian */
         "lv", LANGUAGE_lv, /* Latvian */
         "mk", LANGUAGE_mk, /* Macedonian */
         "mn", LANGUAGE_mn, /* Mongolian */
         "nb", LANGUAGE_nb, /* Norwegian Bokmål */
+        "ne", LANGUAGE_ne, /* Nepali */
         "nl", LANGUAGE_nl, /* Dutch */
         "pl", LANGUAGE_pl, /* Polish */
         "pt", LANGUAGE_pt, /* Portuguese */
diff --git a/java/src/com/android/inputmethod/keyboard/internal/RecentsKeyboard.java b/java/src/com/android/inputmethod/keyboard/internal/RecentsKeyboard.java
deleted file mode 100644
index 629c604..0000000
--- a/java/src/com/android/inputmethod/keyboard/internal/RecentsKeyboard.java
+++ /dev/null
@@ -1,157 +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.
- */
-
-package com.android.inputmethod.keyboard.internal;
-
-import android.text.TextUtils;
-
-import com.android.inputmethod.keyboard.Key;
-import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.latin.utils.CollectionUtils;
-
-import java.util.ArrayDeque;
-import java.util.Random;
-
-/**
- * This is a Keyboard class to host recently used keys.
- */
-// TODO: Save/restore recent keys from/to preferences.
-public class RecentsKeyboard extends Keyboard {
-    private static final int TEMPLATE_KEY_CODE_0 = 0x30;
-    private static final int TEMPLATE_KEY_CODE_1 = 0x31;
-
-    private final int mLeftPadding;
-    private final int mHorizontalStep;
-    private final int mVerticalStep;
-    private final int mColumnsNum;
-    private final int mMaxRecentKeyCount;
-    private final ArrayDeque<RecentKey> mRecentKeys = CollectionUtils.newArrayDeque();
-
-    private Key[] mCachedRecentKeys;
-
-    public RecentsKeyboard(final Keyboard templateKeyboard) {
-        super(templateKeyboard);
-        final Key key0 = getTemplateKey(TEMPLATE_KEY_CODE_0);
-        final Key key1 = getTemplateKey(TEMPLATE_KEY_CODE_1);
-        mLeftPadding = key0.getX();
-        mHorizontalStep = Math.abs(key1.getX() - key0.getX());
-        mVerticalStep = key0.getHeight() + mVerticalGap;
-        mColumnsNum = mBaseWidth / mHorizontalStep;
-        final int rowsNum = mBaseHeight / mVerticalStep;
-        mMaxRecentKeyCount = mColumnsNum * rowsNum;
-    }
-
-    private Key getTemplateKey(final int code) {
-        for (final Key key : super.getKeys()) {
-            if (key.getCode() == code) {
-                return key;
-            }
-        }
-        throw new RuntimeException("Can't find template key: code=" + code);
-    }
-
-    private final Random random = new Random();
-
-    public void addRecentKey(final Key usedKey) {
-        synchronized (mRecentKeys) {
-            mCachedRecentKeys = null;
-            final RecentKey key = (usedKey instanceof RecentKey)
-                    ? (RecentKey)usedKey : new RecentKey(usedKey);
-            while (mRecentKeys.remove(key)) {
-                // Remove duplicate keys.
-            }
-            mRecentKeys.addFirst(key);
-            while (mRecentKeys.size() > mMaxRecentKeyCount) {
-                mRecentKeys.removeLast();
-            }
-            int index = 0;
-            for (final RecentKey recentKey : mRecentKeys) {
-                final int keyX = getKeyX(index);
-                final int keyY = getKeyY(index);
-                final int x = keyX+random.nextInt(recentKey.getWidth());
-                final int y = keyY+random.nextInt(recentKey.getHeight());
-                recentKey.updateCorrdinates(keyX, keyY);
-                index++;
-            }
-        }
-    }
-
-    private int getKeyX(final int index) {
-        final int column = index % mColumnsNum;
-        return column * mHorizontalStep + mLeftPadding;
-    }
-
-    private int getKeyY(final int index) {
-        final int row = index / mColumnsNum;
-        return row * mVerticalStep + mTopPadding;
-    }
-
-    @Override
-    public Key[] getKeys() {
-        synchronized (mRecentKeys) {
-            if (mCachedRecentKeys != null) {
-                return mCachedRecentKeys;
-            }
-            mCachedRecentKeys = mRecentKeys.toArray(new Key[mRecentKeys.size()]);
-            return mCachedRecentKeys;
-        }
-    }
-
-    @Override
-    public Key[] getNearestKeys(final int x, final int y) {
-        // TODO: Calculate the nearest key index in mRecentKeys from x and y.
-        return getKeys();
-    }
-
-    static final class RecentKey extends Key {
-        private int mCurrentX;
-        private int mCurrentY;
-
-        public RecentKey(final Key originalKey) {
-            super(originalKey);
-        }
-
-        public void updateCorrdinates(final int x, final int y) {
-            mCurrentX = x;
-            mCurrentY = y;
-            getHitBox().offsetTo(x, y);
-        }
-
-        @Override
-        public int getX() {
-            return mCurrentX;
-        }
-
-        @Override
-        public int getY() {
-            return mCurrentY;
-        }
-
-        @Override
-        public boolean equals(final Object o) {
-            if (!(o instanceof Key)) return false;
-            final Key key = (Key)o;
-            if (getCode() != key.getCode()) return false;
-            if (!TextUtils.equals(getLabel(), key.getLabel())) return false;
-            return TextUtils.equals(getOutputText(), key.getOutputText());
-        }
-
-        @Override
-        public String toString() {
-            return "RecentKey: " + super.toString();
-        }
-    }
-}
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index e8b0657..b49cd80 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -19,6 +19,7 @@
 import android.text.TextUtils;
 import android.util.SparseArray;
 
+import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.keyboard.ProximityInfo;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.settings.NativeSuggestOptions;
@@ -40,14 +41,20 @@
     private static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH;
     // Must be equal to MAX_RESULTS in native/jni/src/defines.h
     private static final int MAX_RESULTS = 18;
+    // Required space count for auto commit.
+    // TODO: Remove this heuristic.
+    private static final int SPACE_COUNT_FOR_AUTO_COMMIT = 3;
 
     private long mNativeDict;
     private final Locale mLocale;
+    private final long mDictSize;
+    private final String mDictFilePath;
     private final int[] mInputCodePoints = new int[MAX_WORD_LENGTH];
     private final int[] mOutputCodePoints = new int[MAX_WORD_LENGTH * MAX_RESULTS];
     private final int[] mSpaceIndices = new int[MAX_RESULTS];
     private final int[] mOutputScores = new int[MAX_RESULTS];
     private final int[] mOutputTypes = new int[MAX_RESULTS];
+    private final int[] mOutputAutoCommitFirstWordConfidence = new int[MAX_RESULTS];
 
     private final NativeSuggestOptions mNativeSuggestOptions = new NativeSuggestOptions();
 
@@ -62,7 +69,7 @@
             if (traverseSession == null) {
                 traverseSession = mDicTraverseSessions.get(traverseSessionId);
                 if (traverseSession == null) {
-                    traverseSession = new DicTraverseSession(mLocale, mNativeDict);
+                    traverseSession = new DicTraverseSession(mLocale, mNativeDict, mDictSize);
                     mDicTraverseSessions.put(traverseSessionId, traverseSession);
                 }
             }
@@ -85,6 +92,8 @@
             final boolean isUpdatable) {
         super(dictType);
         mLocale = locale;
+        mDictSize = length;
+        mDictFilePath = filename;
         mNativeSuggestOptions.setUseFullEditDistance(useFullEditDistance);
         loadDictionary(filename, offset, length, isUpdatable);
     }
@@ -95,6 +104,9 @@
 
     private static native long openNative(String sourceDir, long dictOffset, long dictSize,
             boolean isUpdatable);
+    private static native void flushNative(long dict, String filePath);
+    private static native boolean needsToRunGCNative(long dict);
+    private static native void flushWithGCNative(long dict, String filePath);
     private static native void closeNative(long dict);
     private static native int getProbabilityNative(long dict, int[] word);
     private static native boolean isValidBigramNative(long dict, int[] word0, int[] word1);
@@ -102,7 +114,8 @@
             long traverseSession, int[] xCoordinates, int[] yCoordinates, int[] times,
             int[] pointerIds, int[] inputCodePoints, int inputSize, int commitPoint,
             int[] suggestOptions, int[] prevWordCodePointArray,
-            int[] outputCodePoints, int[] outputScores, int[] outputIndices, int[] outputTypes);
+            int[] outputCodePoints, int[] outputScores, int[] outputIndices, int[] outputTypes,
+            int[] outputAutoCommitFirstWordConfidence);
     private static native float calcNormalizedScoreNative(int[] before, int[] after, int score);
     private static native int editDistanceNative(int[] before, int[] after);
     private static native void addUnigramWordNative(long dict, int[] word, int probability);
@@ -155,7 +168,7 @@
                 ips.getYCoordinates(), ips.getTimes(), ips.getPointerIds(), mInputCodePoints,
                 inputSize, 0 /* commitPoint */, mNativeSuggestOptions.getOptions(),
                 prevWordCodePointArray, mOutputCodePoints, mOutputScores, mSpaceIndices,
-                mOutputTypes);
+                mOutputTypes, mOutputAutoCommitFirstWordConfidence);
         final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
         for (int j = 0; j < count; ++j) {
             final int start = j * MAX_WORD_LENGTH;
@@ -179,7 +192,8 @@
                 // flags too and pass mOutputTypes[j] instead of kind
                 suggestions.add(new SuggestedWordInfo(new String(mOutputCodePoints, start, len),
                         score, kind, this /* sourceDict */,
-                        mSpaceIndices[0] /* indexOfTouchPointOfSecondWord */));
+                        mSpaceIndices[j] /* indexOfTouchPointOfSecondWord */,
+                        mOutputAutoCommitFirstWordConfidence[0]));
             }
         }
         return suggestions;
@@ -253,6 +267,40 @@
         removeBigramWordsNative(mNativeDict, codePoints0, codePoints1);
     }
 
+    @UsedForTesting
+    public void flush() {
+        if (!isValidDictionary()) return;
+        flushNative(mNativeDict, mDictFilePath);
+    }
+
+    @UsedForTesting
+    public void flushWithGC() {
+        if (!isValidDictionary()) return;
+        flushWithGCNative(mNativeDict, mDictFilePath);
+    }
+
+    @UsedForTesting
+    public boolean needsToRunGC() {
+        if (!isValidDictionary()) return false;
+        return needsToRunGCNative(mNativeDict);
+    }
+
+    @Override
+    public boolean shouldAutoCommit(final SuggestedWordInfo candidate) {
+        // TODO: actually use the confidence rather than use this completely broken heuristic
+        final String word = candidate.mWord;
+        final int length = word.length();
+        int remainingSpaces = SPACE_COUNT_FOR_AUTO_COMMIT;
+        for (int i = 0; i < length; ++i) {
+            // This is okay because no low-surrogate and no high-surrogate can ever match the
+            // space character, so we don't need to take care of iterating on code points.
+            if (Constants.CODE_SPACE == word.charAt(i)) {
+                if (0 >= --remainingSpaces) return true;
+            }
+        }
+        return false;
+    }
+
     @Override
     public void close() {
         synchronized (mDicTraverseSessions) {
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
index 2b6d983..5661842 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
@@ -21,9 +21,10 @@
 import android.content.res.AssetFileDescriptor;
 import android.util.Log;
 
+import com.android.inputmethod.latin.makedict.DictDecoder;
+import com.android.inputmethod.latin.makedict.FormatSpec;
 import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
 import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
-import com.android.inputmethod.latin.makedict.Ver3DictDecoder;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 import com.android.inputmethod.latin.utils.DictionaryInfoUtils;
 import com.android.inputmethod.latin.utils.LocaleUtils;
@@ -228,7 +229,7 @@
     private static boolean hackCanUseDictionaryFile(final Locale locale, final File f) {
         try {
             // Read the version of the file
-            final Ver3DictDecoder dictDecoder = new Ver3DictDecoder(f);
+            final DictDecoder dictDecoder = FormatSpec.getDictDecoder(f);
             final FileHeader header = dictDecoder.readHeader();
 
             final String version = header.mDictionaryOptions.mAttributes.get(VERSION_KEY);
diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java
index 8aec03f..029ba02 100644
--- a/java/src/com/android/inputmethod/latin/Constants.java
+++ b/java/src/com/android/inputmethod/latin/Constants.java
@@ -220,7 +220,11 @@
         }
     }
 
+    public static final int MAX_INT_BIT_COUNT = 32;
+    public static final String STRING_COMMA = ",";
+
     private Constants() {
         // This utility class is not publicly instantiable.
     }
+
 }
diff --git a/java/src/com/android/inputmethod/latin/DicTraverseSession.java b/java/src/com/android/inputmethod/latin/DicTraverseSession.java
index 45b2813..8d295ad 100644
--- a/java/src/com/android/inputmethod/latin/DicTraverseSession.java
+++ b/java/src/com/android/inputmethod/latin/DicTraverseSession.java
@@ -25,16 +25,16 @@
         JniUtils.loadNativeLibrary();
     }
 
-    private static native long setDicTraverseSessionNative(String locale);
+    private static native long setDicTraverseSessionNative(String locale, long dictSize);
     private static native void initDicTraverseSessionNative(long nativeDicTraverseSession,
             long dictionary, int[] previousWord, int previousWordLength);
     private static native void releaseDicTraverseSessionNative(long nativeDicTraverseSession);
 
     private long mNativeDicTraverseSession;
 
-    public DicTraverseSession(Locale locale, long dictionary) {
+    public DicTraverseSession(Locale locale, long dictionary, long dictSize) {
         mNativeDicTraverseSession = createNativeDicTraverseSession(
-                locale != null ? locale.toString() : "");
+                locale != null ? locale.toString() : "", dictSize);
         initSession(dictionary);
     }
 
@@ -51,8 +51,8 @@
                 mNativeDicTraverseSession, dictionary, previousWord, previousWordLength);
     }
 
-    private final long createNativeDicTraverseSession(String locale) {
-        return setDicTraverseSessionNative(locale);
+    private final long createNativeDicTraverseSession(String locale, long dictSize) {
+        return setDicTraverseSessionNative(locale, dictSize);
     }
 
     private void closeInternal() {
diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java
index 8a3a884..fa79f5a 100644
--- a/java/src/com/android/inputmethod/latin/Dictionary.java
+++ b/java/src/com/android/inputmethod/latin/Dictionary.java
@@ -137,7 +137,10 @@
     }
 
     /**
-     * Whether we think this suggestion should trigger an auto-commit.
+     * Whether we think this suggestion should trigger an auto-commit. prevWord is the word
+     * before the suggestion, so that we can use n-gram frequencies.
+     * @param candidate The candidate suggestion, in whole (not only the first part).
+     * @return whether we should auto-commit or not.
      */
     public boolean shouldAutoCommit(final SuggestedWordInfo candidate) {
         // If we don't have support for auto-commit, or if we don't know, we return false to
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index c884e7b..2a90764 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -617,4 +617,14 @@
         });
         return holder.get(false, TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS);
     }
+
+    @UsedForTesting
+    public void shutdownExecutorForTests() {
+        getExecutor(mFilename).shutdown();
+    }
+
+    @UsedForTesting
+    public boolean isTerminatedForTests() {
+        return getExecutor(mFilename).isTerminated();
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
index ba7d1a2..d491f98 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
@@ -344,7 +344,8 @@
             // in the future.
             suggestions.add(new SuggestedWordInfo(new String(word, 0, depth + 1), finalFreq,
                     SuggestedWordInfo.KIND_CORRECTION, this /* sourceDict */,
-                    SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */));
+                    SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+                    SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
             if (suggestions.size() >= Suggest.MAX_SUGGESTIONS) return false;
         }
         if (null != node.mShortcutTargets) {
@@ -353,7 +354,8 @@
                 final char[] shortcut = node.mShortcutTargets.get(shortcutIndex);
                 suggestions.add(new SuggestedWordInfo(new String(shortcut, 0, shortcut.length),
                         finalFreq, SuggestedWordInfo.KIND_SHORTCUT, this /* sourceDict */,
-                        SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */));
+                        SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+                        SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
                 if (suggestions.size() > Suggest.MAX_SUGGESTIONS) return false;
             }
         }
@@ -604,7 +606,8 @@
                 suggestions.add(new SuggestedWordInfo(new String(mLookedUpString, index,
                         Constants.DICTIONARY_MAX_WORD_LENGTH - index),
                         freq, SuggestedWordInfo.KIND_CORRECTION, this /* sourceDict */,
-                        SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */));
+                        SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+                        SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
             }
         }
     }
diff --git a/java/src/com/android/inputmethod/latin/InputPointers.java b/java/src/com/android/inputmethod/latin/InputPointers.java
index e96a46e..2e638aa 100644
--- a/java/src/com/android/inputmethod/latin/InputPointers.java
+++ b/java/src/com/android/inputmethod/latin/InputPointers.java
@@ -105,6 +105,17 @@
         mTimes.append(times, startPos, length);
     }
 
+    /**
+     * Shift to the left by elementCount, discarding elementCount pointers at the start.
+     * @param elementCount how many elements to shift.
+     */
+    public void shift(final int elementCount) {
+        mXCoordinates.shift(elementCount);
+        mYCoordinates.shift(elementCount);
+        mPointerIds.shift(elementCount);
+        mTimes.shift(elementCount);
+    }
+
     public void reset() {
         final int defaultCapacity = mDefaultCapacity;
         mXCoordinates.reset(defaultCapacity);
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 6c83ac7e..d3a18d4 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -95,7 +95,6 @@
 import com.android.inputmethod.latin.utils.IntentUtils;
 import com.android.inputmethod.latin.utils.JniUtils;
 import com.android.inputmethod.latin.utils.LatinImeLoggerUtils;
-import com.android.inputmethod.latin.utils.PositionalInfoForUserDictPendingAddition;
 import com.android.inputmethod.latin.utils.RecapitalizeStatus;
 import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper;
 import com.android.inputmethod.latin.utils.TargetPackageInfoGetterTask;
@@ -185,8 +184,6 @@
     private boolean mIsUserDictionaryAvailable;
 
     private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
-    private PositionalInfoForUserDictPendingAddition
-            mPositionalInfoForUserDictPendingAddition = null;
     private final WordComposer mWordComposer = new WordComposer();
     private final RichInputConnection mConnection = new RichInputConnection(this);
     private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus();
@@ -898,19 +895,6 @@
                 currentSettingsValues.mGestureTrailEnabled,
                 currentSettingsValues.mGestureFloatingPreviewTextEnabled);
 
-        // If we have a user dictionary addition in progress, we should check now if we should
-        // replace the previously committed string with the word that has actually been added
-        // to the user dictionary.
-        if (null != mPositionalInfoForUserDictPendingAddition
-                && mPositionalInfoForUserDictPendingAddition.tryReplaceWithActualWord(
-                        mConnection, editorInfo, mLastSelectionEnd, currentLocale)) {
-            mPositionalInfoForUserDictPendingAddition = null;
-        }
-        // If tryReplaceWithActualWord returns false, we don't know what word was
-        // added to the user dictionary yet, so we keep the data and defer processing. The word will
-        // be replaced when the user dictionary reports back with the actual word, which ends
-        // up calling #onWordAddedToUserDictionary() in this class.
-
         initPersonalizationDebugSettings(currentSettingsValues);
 
         if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
@@ -1250,7 +1234,10 @@
         int visibleTopY = extraHeight;
         // Need to set touchable region only if input view is being shown
         if (visibleKeyboardView.isShown()) {
-            if (mSuggestionStripView.getVisibility() == View.VISIBLE) {
+            // Note that the height of Emoji layout is the same as the height of the main keyboard
+            // and the suggestion strip
+            if (mKeyboardSwitcher.isShowingEmojiKeyboard()
+                    || mSuggestionStripView.getVisibility() == View.VISIBLE) {
                 visibleTopY -= suggestionsHeight;
             }
             final int touchY = mKeyboardSwitcher.isShowingMoreKeysPanel() ? 0 : visibleTopY;
@@ -1416,7 +1403,6 @@
     public void addWordToUserDictionary(final String word) {
         if (TextUtils.isEmpty(word)) {
             // Probably never supposed to happen, but just in case.
-            mPositionalInfoForUserDictPendingAddition = null;
             return;
         }
         final String wordToEdit;
@@ -1428,22 +1414,6 @@
         mUserDictionary.addWordToUserDictionary(wordToEdit);
     }
 
-    public void onWordAddedToUserDictionary(final String newSpelling) {
-        // If word was added but not by us, bail out
-        if (null == mPositionalInfoForUserDictPendingAddition) return;
-        if (mWordComposer.isComposingWord()) {
-            // We are late... give up and return
-            mPositionalInfoForUserDictPendingAddition = null;
-            return;
-        }
-        mPositionalInfoForUserDictPendingAddition.setActualWordBeingAdded(newSpelling);
-        if (mPositionalInfoForUserDictPendingAddition.tryReplaceWithActualWord(
-                mConnection, getCurrentInputEditorInfo(), mLastSelectionEnd,
-                mSubtypeSwitcher.getCurrentSubtypeLocale())) {
-            mPositionalInfoForUserDictPendingAddition = null;
-        }
-    }
-
     private void onSettingsKeyPressed() {
         if (isShowingOptionDialog()) return;
         showSubtypeSelectorAndSettings();
@@ -1877,10 +1847,18 @@
 
     @Override
     public void onUpdateBatchInput(final InputPointers batchPointers) {
-        final SuggestedWordInfo candidate = mSuggestedWords.getAutoCommitCandidate();
-        if (null != candidate) {
-            if (candidate.mSourceDict.shouldAutoCommit(candidate)) {
-                // TODO: implement auto-commit
+        if (mSettings.getCurrent().mPhraseGestureEnabled) {
+            final SuggestedWordInfo candidate = mSuggestedWords.getAutoCommitCandidate();
+            if (null != candidate) {
+                if (candidate.mSourceDict.shouldAutoCommit(candidate)) {
+                    final String[] commitParts = candidate.mWord.split(" ", 2);
+                    batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord);
+                    promotePhantomSpace();
+                    mConnection.commitText(commitParts[0], 0);
+                    mSpaceState = SPACE_STATE_PHANTOM;
+                    mKeyboardSwitcher.updateShiftState();
+                    mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
+                }
             }
         }
         mInputUpdater.onUpdateBatchInput(batchPointers);
@@ -1893,12 +1871,24 @@
         if (TextUtils.isEmpty(batchInputText)) {
             return;
         }
-        mWordComposer.setBatchInputWord(batchInputText);
         mConnection.beginBatchEdit();
         if (SPACE_STATE_PHANTOM == mSpaceState) {
             promotePhantomSpace();
         }
-        mConnection.setComposingText(batchInputText, 1);
+        if (mSettings.getCurrent().mPhraseGestureEnabled) {
+            // Find the last space
+            final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1;
+            if (0 != indexOfLastSpace) {
+                mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), 1);
+                showSuggestionStrip(suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture());
+            }
+            final String lastWord = batchInputText.substring(indexOfLastSpace);
+            mWordComposer.setBatchInputWord(lastWord);
+            mConnection.setComposingText(lastWord, 1);
+        } else {
+            mWordComposer.setBatchInputWord(batchInputText);
+            mConnection.setComposingText(batchInputText, 1);
+        }
         mExpectingUpdateSelection = true;
         mConnection.endBatchEdit();
         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
@@ -2704,6 +2694,8 @@
         // recorrection. This is a temporary, stopgap measure that will be removed later.
         // TODO: remove this.
         if (mAppWorkAroundsUtils.isBrokenByRecorrection()) return;
+        // A simple way to test for support from the TextView.
+        if (!isSuggestionsStripVisible()) return;
         // Recorrection is not supported in languages without spaces because we don't know
         // how to segment them yet.
         if (!mSettings.getCurrent().mCurrentLanguageHasSpaces) return;
@@ -2730,7 +2722,9 @@
                     suggestions.add(new SuggestedWordInfo(s,
                             SuggestionStripView.MAX_SUGGESTIONS - i,
                             SuggestedWordInfo.KIND_RESUMED, Dictionary.DICTIONARY_RESUMED,
-                            SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */));
+                            SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+                            SuggestedWordInfo.NOT_A_CONFIDENCE
+                                    /* autoCommitFirstWordConfidence */));
                 }
             }
         }
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index 18ba158..7815f4d 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -48,8 +48,9 @@
 
     // Session id for
     // {@link #getSuggestedWords(WordComposer,String,ProximityInfo,boolean,int)}.
+    // We are sharing the same ID between typing and gesture to save RAM footprint.
     public static final int SESSION_TYPING = 0;
-    public static final int SESSION_GESTURE = 1;
+    public static final int SESSION_GESTURE = 0;
 
     // TODO: rename this to CORRECTION_OFF
     public static final int CORRECTION_NONE = 0;
@@ -326,7 +327,8 @@
             suggestionsContainer.add(0, new SuggestedWordInfo(typedWord,
                     SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_TYPED,
                     Dictionary.DICTIONARY_USER_TYPED,
-                    SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */));
+                    SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+                    SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
         }
         SuggestedWordInfo.removeDups(suggestionsContainer);
 
@@ -473,7 +475,8 @@
             sb.appendCodePoint(Constants.CODE_SINGLE_QUOTE);
         }
         return new SuggestedWordInfo(sb.toString(), wordInfo.mScore, wordInfo.mKind,
-                wordInfo.mSourceDict, wordInfo.mIndexOfTouchPointOfSecondWord);
+                wordInfo.mSourceDict, wordInfo.mIndexOfTouchPointOfSecondWord,
+                SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */);
     }
 
     public void close() {
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index b27fd81..fed4cdb 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -114,7 +114,8 @@
             final SuggestedWordInfo suggestedWordInfo = new SuggestedWordInfo(text.toString(),
                     SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_APP_DEFINED,
                     Dictionary.DICTIONARY_APPLICATION_DEFINED,
-                    SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */);
+                    SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+                    SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */);
             result.add(suggestedWordInfo);
         }
         return result;
@@ -128,7 +129,8 @@
         final HashSet<String> alreadySeen = CollectionUtils.newHashSet();
         suggestionsList.add(new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE,
                 SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED,
-                SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */));
+                SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+                SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
         alreadySeen.add(typedWord.toString());
         final int previousSize = previousSuggestions.size();
         for (int index = 1; index < previousSize; index++) {
@@ -151,6 +153,7 @@
 
     public static final class SuggestedWordInfo {
         public static final int NOT_AN_INDEX = -1;
+        public static final int NOT_A_CONFIDENCE = -1;
         public static final int MAX_SCORE = Integer.MAX_VALUE;
         public static final int KIND_MASK_KIND = 0xFF; // Mask to get only the kind
         public static final int KIND_TYPED = 0; // What user typed
@@ -180,16 +183,30 @@
         // passed to native code to get suggestions for a gesture that corresponds to the first
         // letter of the second word.
         public final int mIndexOfTouchPointOfSecondWord;
+        // For auto-commit. This is a measure of how confident we are that we can commit the
+        // first word of this suggestion.
+        public final int mAutoCommitFirstWordConfidence;
         private String mDebugString = "";
 
+        /**
+         * Create a new suggested word info.
+         * @param word The string to suggest.
+         * @param score A measure of how likely this suggestion is.
+         * @param kind The kind of suggestion, as one of the above KIND_* constants.
+         * @param sourceDict What instance of Dictionary produced this suggestion.
+         * @param indexOfTouchPointOfSecondWord See mIndexOfTouchPointOfSecondWord.
+         * @param autoCommitFirstWordConfidence See mAutoCommitFirstWordConfidence.
+         */
         public SuggestedWordInfo(final String word, final int score, final int kind,
-                final Dictionary sourceDict, final int indexOfTouchPointOfSecondWord) {
+                final Dictionary sourceDict, final int indexOfTouchPointOfSecondWord,
+                final int autoCommitFirstWordConfidence) {
             mWord = word;
             mScore = score;
             mKind = kind;
             mSourceDict = sourceDict;
             mCodePointCount = StringUtils.codePointCount(mWord);
             mIndexOfTouchPointOfSecondWord = indexOfTouchPointOfSecondWord;
+            mAutoCommitFirstWordConfidence = autoCommitFirstWordConfidence;
         }
 
         public boolean isEligibleForAutoCommit() {
@@ -259,4 +276,24 @@
                 false /* willAutoCorrect */, mIsPunctuationSuggestions, mIsObsoleteSuggestions,
                 mIsPrediction);
     }
+
+    // Creates a new SuggestedWordInfo from the currently suggested words that removes all but the
+    // last word of all suggestions, separated by a space. This is necessary because when we commit
+    // a multiple-word suggestion, the IME only retains the last word as the composing word, and
+    // we should only suggest replacements for this last word.
+    // TODO: make this work with languages without spaces.
+    public SuggestedWords getSuggestedWordsForLastWordOfPhraseGesture() {
+        final ArrayList<SuggestedWordInfo> newSuggestions = CollectionUtils.newArrayList();
+        for (int i = 0; i < mSuggestedWordInfoList.size(); ++i) {
+            final SuggestedWordInfo info = mSuggestedWordInfoList.get(i);
+            final int indexOfLastSpace = info.mWord.lastIndexOf(Constants.CODE_SPACE) + 1;
+            final String lastWord = info.mWord.substring(indexOfLastSpace);
+            newSuggestions.add(new SuggestedWordInfo(lastWord, info.mScore, info.mKind,
+                    info.mSourceDict, SuggestedWordInfo.NOT_AN_INDEX,
+                    SuggestedWordInfo.NOT_A_CONFIDENCE));
+        }
+        return new SuggestedWords(newSuggestions, mTypedWordValid,
+                mWillAutoCorrect, mIsPunctuationSuggestions, mIsObsoleteSuggestions,
+                mIsPrediction);
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
index b2bb615..a241b55 100644
--- a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
@@ -103,14 +103,6 @@
             @Override
             public void onChange(final boolean self, final Uri uri) {
                 setRequiresReload(true);
-                // We want to report back to Latin IME in case the user just entered the word.
-                // If the user changed the word in the dialog box, then we want to replace
-                // what was entered in the text field.
-                if (null == uri || !(context instanceof LatinIME)) return;
-                final long changedRowId = ContentUris.parseId(uri);
-                if (-1 == changedRowId) return; // Unknown content... Not sure why we're here
-                final String changedWord = getChangedWordForUri(uri);
-                ((LatinIME)context).onWordAddedToUserDictionary(changedWord);
             }
         };
         cres.registerContentObserver(Words.CONTENT_URI, true, mObserver);
@@ -118,19 +110,6 @@
         loadDictionary();
     }
 
-    private String getChangedWordForUri(final Uri uri) {
-        final Cursor cursor = mContext.getContentResolver().query(uri,
-                PROJECTION_QUERY, null, null, null);
-        if (cursor == null) return null;
-        try {
-            if (!cursor.moveToFirst()) return null;
-            final int indexWord = cursor.getColumnIndex(Words.WORD);
-            return cursor.getString(indexWord);
-        } finally {
-            cursor.close();
-        }
-    }
-
     @Override
     public synchronized void close() {
         if (mObserver != null) {
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java
index ceb8fa8..5b319ad 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java
@@ -342,13 +342,11 @@
      * @param formatOptions file format options.
      * @return the word with its frequency, as a weighted string.
      */
-    /* package for tests */ static WeightedString getWordAtPosition(
-            final Ver3DictDecoder dictDecoder, final int headerSize, final int pos,
-            final FormatOptions formatOptions) {
-        final DictBuffer dictBuffer = dictDecoder.getDictBuffer();
+    /* package for tests */ static WeightedString getWordAtPosition(final DictDecoder dictDecoder,
+            final int headerSize, final int pos, final FormatOptions formatOptions) {
         final WeightedString result;
-        final int originalPos = dictBuffer.position();
-        dictBuffer.position(pos);
+        final int originalPos = dictDecoder.getPosition();
+        dictDecoder.setPosition(pos);
 
         if (BinaryDictIOUtils.supportsDynamicUpdate(formatOptions)) {
             result = getWordAtPositionWithParentAddress(dictDecoder, pos, formatOptions);
@@ -357,14 +355,13 @@
                     formatOptions);
         }
 
-        dictBuffer.position(originalPos);
+        dictDecoder.setPosition(originalPos);
         return result;
     }
 
     @SuppressWarnings("unused")
-    private static WeightedString getWordAtPositionWithParentAddress(
-            final Ver3DictDecoder dictDecoder, final int pos, final FormatOptions options) {
-        final DictBuffer dictBuffer = dictDecoder.getDictBuffer();
+    private static WeightedString getWordAtPositionWithParentAddress(final DictDecoder dictDecoder,
+            final int pos, final FormatOptions options) {
         int currentPos = pos;
         int frequency = Integer.MIN_VALUE;
         final StringBuilder builder = new StringBuilder();
@@ -373,7 +370,7 @@
             PtNodeInfo currentInfo;
             int loopCounter = 0;
             do {
-                dictBuffer.position(currentPos);
+                dictDecoder.setPosition(currentPos);
                 currentInfo = dictDecoder.readPtNode(currentPos, options);
                 if (BinaryDictIOUtils.isMovedPtNode(currentInfo.mFlags, options)) {
                     currentPos = currentInfo.mParentAddress + currentInfo.mOriginalAddress;
@@ -392,11 +389,10 @@
     }
 
     private static WeightedString getWordAtPositionWithoutParentAddress(
-            final Ver3DictDecoder dictDecoder, final int headerSize, final int pos,
+            final DictDecoder dictDecoder, final int headerSize, final int pos,
             final FormatOptions options) {
-        final DictBuffer dictBuffer = dictDecoder.getDictBuffer();
-        dictBuffer.position(headerSize);
-        final int count = readPtNodeCount(dictBuffer);
+        dictDecoder.setPosition(headerSize);
+        final int count = dictDecoder.readPtNodeCount();
         int groupPos = headerSize + BinaryDictIOUtils.getPtNodeCountSize(count);
         final StringBuilder builder = new StringBuilder();
         WeightedString result = null;
@@ -414,8 +410,8 @@
                 if (info.mChildrenAddress > pos) {
                     if (null == last) continue;
                     builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
-                    dictBuffer.position(last.mChildrenAddress);
-                    i = readPtNodeCount(dictBuffer);
+                    dictDecoder.setPosition(last.mChildrenAddress);
+                    i = dictDecoder.readPtNodeCount();
                     groupPos = last.mChildrenAddress + BinaryDictIOUtils.getPtNodeCountSize(i);
                     last = null;
                     continue;
@@ -424,8 +420,8 @@
             }
             if (0 == i && BinaryDictIOUtils.hasChildrenAddress(last.mChildrenAddress)) {
                 builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
-                dictBuffer.position(last.mChildrenAddress);
-                i = readPtNodeCount(dictBuffer);
+                dictDecoder.setPosition(last.mChildrenAddress);
+                i = dictDecoder.readPtNodeCount();
                 groupPos = last.mChildrenAddress + BinaryDictIOUtils.getPtNodeCountSize(i);
                 last = null;
                 continue;
@@ -449,17 +445,16 @@
      * @param options file format options.
      * @return the read node array with all his children already read.
      */
-    private static PtNodeArray readNodeArray(final Ver3DictDecoder dictDecoder,
+    private static PtNodeArray readNodeArray(final DictDecoder dictDecoder,
             final int headerSize, final Map<Integer, PtNodeArray> reverseNodeArrayMap,
             final Map<Integer, PtNode> reversePtNodeMap, final FormatOptions options)
             throws IOException {
-        final DictBuffer dictBuffer = dictDecoder.getDictBuffer();
         final ArrayList<PtNode> nodeArrayContents = new ArrayList<PtNode>();
-        final int nodeArrayOriginPos = dictBuffer.position();
+        final int nodeArrayOriginPos = dictDecoder.getPosition();
 
         do { // Scan the linked-list node.
-            final int nodeArrayHeadPos = dictBuffer.position();
-            final int count = readPtNodeCount(dictBuffer);
+            final int nodeArrayHeadPos = dictDecoder.getPosition();
+            final int count = dictDecoder.readPtNodeCount();
             int groupOffsetPos = nodeArrayHeadPos + BinaryDictIOUtils.getPtNodeCountSize(count);
             for (int i = count; i > 0; --i) { // Scan the array of PtNode.
                 PtNodeInfo info = dictDecoder.readPtNode(groupOffsetPos, options);
@@ -480,11 +475,11 @@
                 if (BinaryDictIOUtils.hasChildrenAddress(info.mChildrenAddress)) {
                     PtNodeArray children = reverseNodeArrayMap.get(info.mChildrenAddress);
                     if (null == children) {
-                        final int currentPosition = dictBuffer.position();
-                        dictBuffer.position(info.mChildrenAddress);
+                        final int currentPosition = dictDecoder.getPosition();
+                        dictDecoder.setPosition(info.mChildrenAddress);
                         children = readNodeArray(dictDecoder, headerSize, reverseNodeArrayMap,
                                 reversePtNodeMap, options);
-                        dictBuffer.position(currentPosition);
+                        dictDecoder.setPosition(currentPosition);
                     }
                     nodeArrayContents.add(
                             new PtNode(info.mCharacters, shortcutTargets, bigrams,
@@ -503,15 +498,10 @@
 
             // reach the end of the array.
             if (options.mSupportsDynamicUpdate) {
-                final int nextAddress = dictBuffer.readUnsignedInt24();
-                if (nextAddress >= 0 && nextAddress < dictBuffer.limit()) {
-                    dictBuffer.position(nextAddress);
-                } else {
-                    break;
-                }
+                final boolean hasValidForwardLink = dictDecoder.readForwardLinkAndAdvancePosition();
+                if (!hasValidForwardLink) break;
             }
-        } while (options.mSupportsDynamicUpdate &&
-                dictBuffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS);
+        } while (options.mSupportsDynamicUpdate && dictDecoder.hasNextPtNodeArray());
 
         final PtNodeArray nodeArray = new PtNodeArray(nodeArrayContents);
         nodeArray.mCachedAddressBeforeUpdate = nodeArrayOriginPos;
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java
index 5a21341..f333b0d 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java
@@ -126,8 +126,14 @@
      */
     private static int getPtNodeMaximumSize(final PtNode ptNode, final FormatOptions options) {
         int size = getNodeHeaderSize(ptNode, options);
-        // If terminal, one byte for the frequency
-        if (ptNode.isTerminal()) size += FormatSpec.PTNODE_FREQUENCY_SIZE;
+        if (ptNode.isTerminal()) {
+            // If terminal, one byte for the frequency or four bytes for the terminal id.
+            if (options.mHasTerminalId) {
+                size += FormatSpec.PTNODE_TERMINAL_ID_SIZE;
+            } else {
+                size += FormatSpec.PTNODE_FREQUENCY_SIZE;
+            }
+        }
         size += FormatSpec.PTNODE_MAX_ADDRESS_SIZE; // For children address
         size += getShortcutListSize(ptNode.mShortcutTargets);
         if (null != ptNode.mBigrams) {
@@ -198,6 +204,27 @@
         }
     }
 
+    static int writeUIntToBuffer(final byte[] buffer, int position, final int value,
+            final int size) {
+        switch(size) {
+            case 4:
+                buffer[position++] = (byte) ((value >> 24) & 0xFF);
+                /* fall through */
+            case 3:
+                buffer[position++] = (byte) ((value >> 16) & 0xFF);
+                /* fall through */
+            case 2:
+                buffer[position++] = (byte) ((value >> 8) & 0xFF);
+                /* fall through */
+            case 1:
+                buffer[position++] = (byte) (value & 0xFF);
+                break;
+            default:
+                /* nop */
+        }
+        return position;
+    }
+
     // End utility methods
 
     // This method is responsible for finding a nice ordering of the nodes that favors run-time
@@ -324,7 +351,13 @@
                 changed = true;
             }
             int nodeSize = getNodeHeaderSize(ptNode, formatOptions);
-            if (ptNode.isTerminal()) nodeSize += FormatSpec.PTNODE_FREQUENCY_SIZE;
+            if (ptNode.isTerminal()) {
+                if (formatOptions.mHasTerminalId) {
+                    nodeSize += FormatSpec.PTNODE_TERMINAL_ID_SIZE;
+                } else {
+                    nodeSize += FormatSpec.PTNODE_FREQUENCY_SIZE;
+                }
+            }
             if (formatOptions.mSupportsDynamicUpdate) {
                 nodeSize += FormatSpec.SIGNED_CHILDREN_ADDRESS_SIZE;
             } else if (null != ptNode.mChildren) {
@@ -733,7 +766,7 @@
     }
 
     /**
-     * Write a PtNodeArray to memory. The PtNodeArray is expected to have its final position cached.
+     * Write a PtNodeArray. The PtNodeArray is expected to have its final position cached.
      *
      * @param dict the dictionary the node array is a part of (for relative offsets).
      * @param dictEncoder the dictionary encoder.
@@ -741,7 +774,7 @@
      * @param formatOptions file format options.
      */
     @SuppressWarnings("unused")
-    /* package */ static void writePlacedNode(final FusionDictionary dict,
+    /* package */ static void writePlacedPtNodeArray(final FusionDictionary dict,
             final DictEncoder dictEncoder, final PtNodeArray ptNodeArray,
             final FormatOptions formatOptions) {
         // TODO: Make the code in common with BinaryDictIOUtils#writePtNode
@@ -766,14 +799,7 @@
                         + FormatSpec.MAX_TERMINAL_FREQUENCY
                         + " : " + ptNode.mFrequency);
             }
-
-            dictEncoder.writePtNodeFlags(ptNode, parentPosition, formatOptions);
-            dictEncoder.writeParentPosition(parentPosition, ptNode, formatOptions);
-            dictEncoder.writeCharacters(ptNode.mChars, ptNode.hasSeveralChars());
-            dictEncoder.writeFrequency(ptNode.mFrequency);
-            dictEncoder.writeChildrenPosition(ptNode, formatOptions);
-            dictEncoder.writeShortcuts(ptNode.mShortcutTargets);
-            dictEncoder.writeBigrams(ptNode.mBigrams, dict);
+            dictEncoder.writePtNode(ptNode, parentPosition, formatOptions, dict);
         }
         if (formatOptions.mSupportsDynamicUpdate) {
             dictEncoder.writeForwardLinkAddress(FormatSpec.NO_FORWARD_LINK_ADDRESS);
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
index 106f025..2c5e93e 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
@@ -61,12 +61,11 @@
     /**
      * Retrieves all node arrays without recursive call.
      */
-    private static void readUnigramsAndBigramsBinaryInner(
-            final Ver3DictDecoder dictDecoder, final int headerSize,
-            final Map<Integer, String> words, final Map<Integer, Integer> frequencies,
+    private static void readUnigramsAndBigramsBinaryInner(final DictDecoder dictDecoder,
+            final int headerSize, final Map<Integer, String> words,
+            final Map<Integer, Integer> frequencies,
             final Map<Integer, ArrayList<PendingAttribute>> bigrams,
             final FormatOptions formatOptions) {
-        final DictBuffer dictBuffer = dictDecoder.getDictBuffer();
         int[] pushedChars = new int[FormatSpec.MAX_WORD_LENGTH + 1];
 
         Stack<Position> stack = new Stack<Position>();
@@ -83,11 +82,11 @@
                         p.mNumOfPtNode + ", position=" + p.mPosition + ", length=" + p.mLength);
             }
 
-            if (dictBuffer.position() != p.mAddress) dictBuffer.position(p.mAddress);
+            if (dictDecoder.getPosition() != p.mAddress) dictDecoder.setPosition(p.mAddress);
             if (index != p.mLength) index = p.mLength;
 
             if (p.mNumOfPtNode == Position.NOT_READ_PTNODE_COUNT) {
-                p.mNumOfPtNode = BinaryDictDecoderUtils.readPtNodeCount(dictBuffer);
+                p.mNumOfPtNode = dictDecoder.readPtNodeCount();
                 p.mAddress += getPtNodeCountSize(p.mNumOfPtNode);
                 p.mPosition = 0;
             }
@@ -114,11 +113,12 @@
 
             if (p.mPosition == p.mNumOfPtNode) {
                 if (formatOptions.mSupportsDynamicUpdate) {
-                    final int forwardLinkAddress = dictBuffer.readUnsignedInt24();
-                    if (forwardLinkAddress != FormatSpec.NO_FORWARD_LINK_ADDRESS) {
+                    final boolean hasValidForwardLinkAddress =
+                            dictDecoder.readForwardLinkAndAdvancePosition();
+                    if (hasValidForwardLinkAddress && dictDecoder.hasNextPtNodeArray()) {
                         // The node array has a forward link.
                         p.mNumOfPtNode = Position.NOT_READ_PTNODE_COUNT;
-                        p.mAddress = forwardLinkAddress;
+                        p.mAddress = dictDecoder.getPosition();
                     } else {
                         stack.pop();
                     }
@@ -127,7 +127,7 @@
                 }
             } else {
                 // The Ptnode array has more PtNodes.
-                p.mAddress = dictBuffer.position();
+                p.mAddress = dictDecoder.getPosition();
             }
 
             if (!isMovedPtNode && hasChildrenAddress(info.mChildrenAddress)) {
@@ -148,7 +148,7 @@
      * @throws IOException if the file can't be read.
      * @throws UnsupportedFormatException if the format of the file is not recognized.
      */
-    /* package */ static void readUnigramsAndBigramsBinary(final Ver3DictDecoder dictDecoder,
+    /* package */ static void readUnigramsAndBigramsBinary(final DictDecoder dictDecoder,
             final Map<Integer, String> words, final Map<Integer, Integer> frequencies,
             final Map<Integer, ArrayList<PendingAttribute>> bigrams) throws IOException,
             UnsupportedFormatException {
@@ -169,11 +169,10 @@
      * @throws UnsupportedFormatException if the format of the file is not recognized.
      */
     @UsedForTesting
-    /* package */ static int getTerminalPosition(final Ver3DictDecoder dictDecoder,
+    /* package */ static int getTerminalPosition(final DictDecoder dictDecoder,
             final String word) throws IOException, UnsupportedFormatException {
-        final DictBuffer dictBuffer = dictDecoder.getDictBuffer();
         if (word == null) return FormatSpec.NOT_VALID_WORD;
-        if (dictBuffer.position() != 0) dictBuffer.position(0);
+        dictDecoder.setPosition(0);
 
         final FileHeader header = dictDecoder.readHeader();
         int wordPos = 0;
@@ -182,10 +181,10 @@
             if (wordPos >= wordLen) return FormatSpec.NOT_VALID_WORD;
 
             do {
-                final int ptNodeCount = BinaryDictDecoderUtils.readPtNodeCount(dictBuffer);
+                final int ptNodeCount = dictDecoder.readPtNodeCount();
                 boolean foundNextPtNode = false;
                 for (int i = 0; i < ptNodeCount; ++i) {
-                    final int ptNodePos = dictBuffer.position();
+                    final int ptNodePos = dictDecoder.getPosition();
                     final PtNodeInfo currentInfo = dictDecoder.readPtNode(ptNodePos,
                             header.mFormatOptions);
                     final boolean isMovedNode = isMovedPtNode(currentInfo.mFlags,
@@ -219,7 +218,7 @@
                             return FormatSpec.NOT_VALID_WORD;
                         }
                         foundNextPtNode = true;
-                        dictBuffer.position(currentInfo.mChildrenAddress);
+                        dictDecoder.setPosition(currentInfo.mChildrenAddress);
                         break;
                     }
                 }
@@ -233,11 +232,11 @@
                     return FormatSpec.NOT_VALID_WORD;
                 }
 
-                final int forwardLinkAddress = dictBuffer.readUnsignedInt24();
-                if (forwardLinkAddress == FormatSpec.NO_FORWARD_LINK_ADDRESS) {
+                final boolean hasValidForwardLinkAddress =
+                        dictDecoder.readForwardLinkAndAdvancePosition();
+                if (!hasValidForwardLinkAddress || !dictDecoder.hasNextPtNodeArray()) {
                     return FormatSpec.NOT_VALID_WORD;
                 }
-                dictBuffer.position(forwardLinkAddress);
             } while(true);
         }
         return FormatSpec.NOT_VALID_WORD;
@@ -520,7 +519,7 @@
             final File file, final long offset, final long length)
             throws FileNotFoundException, IOException, UnsupportedFormatException {
         final byte[] buffer = new byte[HEADER_READING_BUFFER_SIZE];
-        final Ver3DictDecoder dictDecoder = new Ver3DictDecoder(file,
+        final DictDecoder dictDecoder = FormatSpec.getDictDecoder(file,
                 new DictDecoder.DictionaryBufferFactory() {
                     @Override
                     public DictBuffer getDictionaryBuffer(File file)
diff --git a/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java
index 11a3f0b..40e8524 100644
--- a/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java
@@ -54,10 +54,13 @@
      * which words from the buffer should be added. If it is null, a new dictionary is created.
      *
      * @param dict an optional dictionary to add words to, or null.
+     * @param deleteDictIfBroken a flag indicating whether this method should remove the broken
+     * dictionary or not.
      * @return the created (or merged) dictionary.
      */
     @UsedForTesting
-    public FusionDictionary readDictionaryBinary(final FusionDictionary dict)
+    public FusionDictionary readDictionaryBinary(final FusionDictionary dict,
+            final boolean deleteDictIfBroken)
             throws FileNotFoundException, IOException, UnsupportedFormatException;
 
     /**
@@ -88,6 +91,41 @@
             final TreeMap<Integer, ArrayList<PendingAttribute>> bigrams)
             throws IOException, UnsupportedFormatException;
 
+    /**
+     * Sets the position of the buffer to the given value.
+     *
+     * @param newPos the new position
+     */
+    public void setPosition(final int newPos);
+
+    /**
+     * Gets the position of the buffer.
+     *
+     * @return the position
+     */
+    public int getPosition();
+
+    /**
+     * Reads and returns the PtNode count out of a buffer and forwards the pointer.
+     */
+    public int readPtNodeCount();
+
+    /**
+     * Reads the forward link and advances the position.
+     *
+     * @return if this method advances the position then true else false.
+     */
+    public boolean readForwardLinkAndAdvancePosition();
+    public boolean hasNextPtNodeArray();
+
+    /**
+     * Opens the dictionary file and makes DictBuffer.
+     */
+    @UsedForTesting
+    public void openDictBuffer() throws FileNotFoundException, IOException;
+    @UsedForTesting
+    public boolean isOpenedDictBuffer();
+
     // Flags for DictionaryBufferFactory.
     public static final int USE_READONLY_BYTEBUFFER = 0x01000000;
     public static final int USE_BYTEARRAY = 0x02000000;
diff --git a/java/src/com/android/inputmethod/latin/makedict/DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/DictEncoder.java
index d1589a3..ea5d492 100644
--- a/java/src/com/android/inputmethod/latin/makedict/DictEncoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/DictEncoder.java
@@ -18,10 +18,8 @@
 
 import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
 import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode;
-import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
 
 import java.io.IOException;
-import java.util.ArrayList;
 
 /**
  * An interface of binary dictionary encoder.
@@ -33,28 +31,8 @@
     public void setPosition(final int position);
     public int getPosition();
     public void writePtNodeCount(final int ptNodeCount);
-    public void writePtNodeFlags(final PtNode ptNode, final int parentAddress,
-           final FormatOptions formatOptions);
-    public void writeParentPosition(final int parentPosition, final PtNode ptNode,
-            final FormatOptions formatOptions);
-    public void writeCharacters(final int[] characters, final boolean hasSeveralChars);
-    public void writeFrequency(final int frequency);
-    public void writeChildrenPosition(final PtNode ptNode, final FormatOptions formatOptions);
-
-    /**
-     * Write a shortcut attributes list to memory.
-     *
-     * @param shortcuts the shortcut attributes list.
-     */
-    public void writeShortcuts(final ArrayList<WeightedString> shortcuts);
-
-    /**
-     * Write a bigram attributes list to memory.
-     *
-     * @param bigrams the bigram attributes list.
-     * @param dict the dictionary the node array is a part of (for relative offsets).
-     */
-    public void writeBigrams(final ArrayList<WeightedString> bigrams, final FusionDictionary dict);
-
     public void writeForwardLinkAddress(final int forwardLinkAddress);
+
+    public void writePtNode(final PtNode ptNode, final int parentPosition,
+            final FormatOptions formatOptions, final FusionDictionary dict);
 }
diff --git a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
index bf35f6a..96ccd8e 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
@@ -18,8 +18,11 @@
 
 import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.makedict.DictDecoder.DictionaryBufferFactory;
 import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions;
 
+import java.io.File;
+
 /**
  * Dictionary File Format Specification.
  */
@@ -195,9 +198,12 @@
 
     public static final int MAGIC_NUMBER = 0x9BC13AFE;
     static final int MINIMUM_SUPPORTED_VERSION = 2;
-    static final int MAXIMUM_SUPPORTED_VERSION = 3;
+    static final int MAXIMUM_SUPPORTED_VERSION = 4;
     static final int NOT_A_VERSION_NUMBER = -1;
     static final int FIRST_VERSION_WITH_DYNAMIC_UPDATE = 3;
+    static final int FIRST_VERSION_WITH_TERMINAL_ID = 4;
+    static final int VERSION3 = 3;
+    static final int VERSION4 = 4;
 
     // These options need to be the same numeric values as the one in the native reading code.
     static final int GERMAN_UMLAUT_PROCESSING_FLAG = 0x1;
@@ -248,11 +254,17 @@
     static final int PTNODE_TERMINATOR_SIZE = 1;
     static final int PTNODE_FLAGS_SIZE = 1;
     static final int PTNODE_FREQUENCY_SIZE = 1;
+    static final int PTNODE_TERMINAL_ID_SIZE = 4;
     static final int PTNODE_MAX_ADDRESS_SIZE = 3;
     static final int PTNODE_ATTRIBUTE_FLAGS_SIZE = 1;
     static final int PTNODE_ATTRIBUTE_MAX_ADDRESS_SIZE = 3;
     static final int PTNODE_SHORTCUT_LIST_SIZE_SIZE = 2;
 
+    // These values are used only by version 4 or later.
+    static final String TRIE_FILE_EXTENSION = ".trie";
+    static final String FREQ_FILE_EXTENSION = ".freq";
+    static final int FREQUENCY_AND_FLAGS_SIZE = 2;
+
     static final int NO_CHILDREN_ADDRESS = Integer.MIN_VALUE;
     static final int NO_PARENT_ADDRESS = 0;
     static final int NO_FORWARD_LINK_ADDRESS = 0;
@@ -261,6 +273,7 @@
     static final int MAX_PTNODES_FOR_ONE_BYTE_PTNODE_COUNT = 0x7F; // 127
     static final int MAX_PTNODES_IN_A_PT_NODE_ARRAY = 0x7FFF; // 32767
     static final int MAX_BIGRAMS_IN_A_PTNODE = 10000;
+    static final int MAX_SHORTCUT_LIST_SIZE_IN_A_PTNODE = 0xFFFF;
 
     static final int MAX_TERMINAL_FREQUENCY = 255;
     static final int MAX_BIGRAM_FREQUENCY = 15;
@@ -284,6 +297,7 @@
     public static final class FormatOptions {
         public final int mVersion;
         public final boolean mSupportsDynamicUpdate;
+        public final boolean mHasTerminalId;
         @UsedForTesting
         public FormatOptions(final int version) {
             this(version, false);
@@ -297,6 +311,7 @@
                         + FIRST_VERSION_WITH_DYNAMIC_UPDATE + " and ulterior.");
             }
             mSupportsDynamicUpdate = supportsDynamicUpdate;
+            mHasTerminalId = (version >= FIRST_VERSION_WITH_TERMINAL_ID);
         }
     }
 
@@ -341,6 +356,28 @@
         }
     }
 
+    /**
+     * Returns new dictionary decoder.
+     *
+     * @param dictFile the dictionary file.
+     * @param bufferType the flag indicating buffer type which is used by the dictionary decoder.
+     * @return new dictionary decoder if the dictionary file exists, otherwise null.
+     */
+    public static DictDecoder getDictDecoder(final File dictFile, final int bufferType) {
+        if (!dictFile.isFile()) return null;
+        return new Ver3DictDecoder(dictFile, bufferType);
+    }
+
+    public static DictDecoder getDictDecoder(final File dictFile,
+            final DictionaryBufferFactory factory) {
+        if (!dictFile.isFile()) return null;
+        return new Ver3DictDecoder(dictFile, factory);
+    }
+
+    public static DictDecoder getDictDecoder(final File dictFile) {
+        return getDictDecoder(dictFile, DictDecoder.USE_READONLY_BYTEBUFFER);
+    }
+
     private FormatSpec() {
         // This utility class is not publicly instantiable.
     }
diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
index 3e685a3..be653fe 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
@@ -111,6 +111,7 @@
         ArrayList<WeightedString> mShortcutTargets;
         ArrayList<WeightedString> mBigrams;
         int mFrequency; // NOT_A_TERMINAL == mFrequency indicates this is not a terminal.
+        int mTerminalId; // NOT_A_TERMINAL == mTerminalId indicates this is not a terminal.
         PtNodeArray mChildren;
         boolean mIsNotAWord; // Only a shortcut
         boolean mIsBlacklistEntry;
@@ -129,6 +130,7 @@
                 final boolean isNotAWord, final boolean isBlacklistEntry) {
             mChars = chars;
             mFrequency = frequency;
+            mTerminalId = frequency;
             mShortcutTargets = shortcutTargets;
             mBigrams = bigrams;
             mChildren = null;
@@ -156,6 +158,10 @@
             mChildren.mData.add(n);
         }
 
+        public int getTerminalId() {
+            return mTerminalId;
+        }
+
         public boolean isTerminal() {
             return NOT_A_TERMINAL != mFrequency;
         }
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java
index 1a5023e..1a90a4b 100644
--- a/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java
@@ -173,11 +173,7 @@
     private final DictionaryBufferFactory mBufferFactory;
     private DictBuffer mDictBuffer;
 
-    public Ver3DictDecoder(final File file) {
-        this(file, USE_READONLY_BYTEBUFFER);
-    }
-
-    public Ver3DictDecoder(final File file, final int factoryFlag) {
+    /* package */ Ver3DictDecoder(final File file, final int factoryFlag) {
         mDictionaryBinaryFile = file;
         mDictBuffer = null;
 
@@ -192,15 +188,21 @@
         }
     }
 
-    public Ver3DictDecoder(final File file, final DictionaryBufferFactory factory) {
+    /* package */ Ver3DictDecoder(final File file, final DictionaryBufferFactory factory) {
         mDictionaryBinaryFile = file;
         mBufferFactory = factory;
     }
 
+    @Override
     public void openDictBuffer() throws FileNotFoundException, IOException {
         mDictBuffer = mBufferFactory.getDictionaryBuffer(mDictionaryBinaryFile);
     }
 
+    @Override
+    public boolean isOpenedDictBuffer() {
+        return mDictBuffer != null;
+    }
+
     /* package */ DictBuffer getDictBuffer() {
         return mDictBuffer;
     }
@@ -306,7 +308,8 @@
     }
 
     @Override
-    public FusionDictionary readDictionaryBinary(final FusionDictionary dict)
+    public FusionDictionary readDictionaryBinary(final FusionDictionary dict,
+            final boolean deleteDictIfBroken)
             throws FileNotFoundException, IOException, UnsupportedFormatException {
         if (mDictBuffer == null) {
             openDictBuffer();
@@ -315,13 +318,13 @@
             return BinaryDictDecoderUtils.readDictionaryBinary(this, dict);
         } catch (IOException e) {
             Log.e(TAG, "The dictionary " + mDictionaryBinaryFile.getName() + " is broken.", e);
-            if (!mDictionaryBinaryFile.delete()) {
+            if (deleteDictIfBroken && !mDictionaryBinaryFile.delete()) {
                 Log.e(TAG, "Failed to delete the broken dictionary.");
             }
             throw e;
         } catch (UnsupportedFormatException e) {
             Log.e(TAG, "The dictionary " + mDictionaryBinaryFile.getName() + " is broken.", e);
-            if (!mDictionaryBinaryFile.delete()) {
+            if (deleteDictIfBroken && !mDictionaryBinaryFile.delete()) {
                 Log.e(TAG, "Failed to delete the broken dictionary.");
             }
             throw e;
@@ -347,4 +350,33 @@
         BinaryDictIOUtils.readUnigramsAndBigramsBinary(this, words, frequencies, bigrams);
     }
 
+    @Override
+    public void setPosition(int newPos) {
+        mDictBuffer.position(newPos);
+    }
+
+    @Override
+    public int getPosition() {
+        return mDictBuffer.position();
+    }
+
+    @Override
+    public int readPtNodeCount() {
+        return BinaryDictDecoderUtils.readPtNodeCount(mDictBuffer);
+    }
+
+    @Override
+    public boolean readForwardLinkAndAdvancePosition() {
+        final int nextAddress = mDictBuffer.readUnsignedInt24();
+        if (nextAddress >= 0 && nextAddress < mDictBuffer.limit()) {
+            mDictBuffer.position(nextAddress);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean hasNextPtNodeArray() {
+        return mDictBuffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS;
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java
index 3f26ff3..222a0f4 100644
--- a/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java
@@ -68,7 +68,7 @@
     @Override
     public void writeDictionary(final FusionDictionary dict, final FormatOptions formatOptions)
             throws IOException, UnsupportedFormatException {
-        if (formatOptions.mVersion > 3) {
+        if (formatOptions.mVersion > FormatSpec.VERSION3) {
             throw new UnsupportedFormatException(
                     "The given format options has wrong version number : "
                     + formatOptions.mVersion);
@@ -103,7 +103,7 @@
         MakedictLog.i("Writing file...");
 
         for (PtNodeArray nodeArray : flatNodes) {
-            BinaryDictEncoderUtils.writePlacedNode(dict, this, nodeArray, formatOptions);
+            BinaryDictEncoderUtils.writePlacedPtNodeArray(dict, this, nodeArray, formatOptions);
         }
         if (MakedictLog.DBG) BinaryDictEncoderUtils.showStatistics(flatNodes);
         mOutStream.write(mBuffer, 0, mPosition);
@@ -126,26 +126,23 @@
     @Override
     public void writePtNodeCount(final int ptNodeCount) {
         final int countSize = BinaryDictIOUtils.getPtNodeCountSize(ptNodeCount);
-        if (1 == countSize) {
-            mBuffer[mPosition++] = (byte) ptNodeCount;
-        } else if (2 == countSize) {
-            mBuffer[mPosition++] = (byte) ((ptNodeCount >> 8) & 0xFF);
-            mBuffer[mPosition++] = (byte) (ptNodeCount & 0xFF);
-        } else {
+        if (countSize != 1 && countSize != 2) {
             throw new RuntimeException("Strange size from getGroupCountSize : " + countSize);
         }
+        mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, ptNodeCount,
+                countSize);
     }
 
-    @Override
-    public void writePtNodeFlags(final PtNode ptNode, final int parentAddress,
+    private void writePtNodeFlags(final PtNode ptNode, final int parentAddress,
             final FormatOptions formatOptions) {
         final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, formatOptions);
-        mBuffer[mPosition++] = BinaryDictEncoderUtils.makePtNodeFlags(ptNode, mPosition,
-                childrenPos, formatOptions);
+        mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition,
+                BinaryDictEncoderUtils.makePtNodeFlags(ptNode, mPosition, childrenPos,
+                        formatOptions),
+                FormatSpec.PTNODE_FLAGS_SIZE);
     }
 
-    @Override
-    public void writeParentPosition(final int parentPosition, final PtNode ptNode,
+    private void writeParentPosition(final int parentPosition, final PtNode ptNode,
             final FormatOptions formatOptions) {
         if (parentPosition == FormatSpec.NO_PARENT_ADDRESS) {
             mPosition = BinaryDictEncoderUtils.writeParentAddress(mBuffer, mPosition,
@@ -156,22 +153,20 @@
         }
     }
 
-    @Override
-    public void writeCharacters(final int[] codePoints, final boolean hasSeveralChars) {
+    private void writeCharacters(final int[] codePoints, final boolean hasSeveralChars) {
         mPosition = CharEncoding.writeCharArray(codePoints, mBuffer, mPosition);
         if (hasSeveralChars) {
             mBuffer[mPosition++] = FormatSpec.PTNODE_CHARACTERS_TERMINATOR;
         }
     }
 
-    @Override
-    public void writeFrequency(final int frequency) {
+    private void writeFrequency(final int frequency) {
         if (frequency >= 0) {
-            mBuffer[mPosition++] = (byte) frequency;
+            mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, frequency,
+                    FormatSpec.PTNODE_FREQUENCY_SIZE);
         }
     }
 
-    @Override
     public void writeChildrenPosition(final PtNode ptNode, final FormatOptions formatOptions) {
         final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, formatOptions);
         if (formatOptions.mSupportsDynamicUpdate) {
@@ -183,8 +178,12 @@
         }
     }
 
-    @Override
-    public void writeShortcuts(final ArrayList<WeightedString> shortcuts) {
+    /**
+     * Write a shortcut attributes list to mBuffer.
+     *
+     * @param shortcuts the shortcut attributes list.
+     */
+    private void writeShortcuts(final ArrayList<WeightedString> shortcuts) {
         if (null == shortcuts || shortcuts.isEmpty()) return;
 
         final int indexOfShortcutByteSize = mPosition;
@@ -195,20 +194,27 @@
             final int shortcutFlags = BinaryDictEncoderUtils.makeShortcutFlags(
                     shortcutIterator.hasNext(),
                     target.mFrequency);
-            mBuffer[mPosition++] = (byte)shortcutFlags;
+            mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, shortcutFlags,
+                    FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE);
             final int shortcutShift = CharEncoding.writeString(mBuffer, mPosition, target.mWord);
             mPosition += shortcutShift;
         }
         final int shortcutByteSize = mPosition - indexOfShortcutByteSize;
-        if (shortcutByteSize > 0xFFFF) {
+        if (shortcutByteSize > FormatSpec.MAX_SHORTCUT_LIST_SIZE_IN_A_PTNODE) {
             throw new RuntimeException("Shortcut list too large");
         }
-        mBuffer[indexOfShortcutByteSize] = (byte)((shortcutByteSize >> 8) & 0xFF);
-        mBuffer[indexOfShortcutByteSize + 1] = (byte)(shortcutByteSize & 0xFF);
+        BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, indexOfShortcutByteSize, shortcutByteSize,
+                FormatSpec.PTNODE_SHORTCUT_LIST_SIZE_SIZE);
     }
 
-    @Override
-    public void writeBigrams(final ArrayList<WeightedString> bigrams, final FusionDictionary dict) {
+    /**
+     * Write a bigram attributes list to mBuffer.
+     *
+     * @param bigrams the bigram attributes list.
+     * @param dict the dictionary the node array is a part of (for relative offsets).
+     */
+    private void writeBigrams(final ArrayList<WeightedString> bigrams,
+            final FusionDictionary dict) {
         if (bigrams == null) return;
 
         final Iterator<WeightedString> bigramIterator = bigrams.iterator();
@@ -220,9 +226,10 @@
             final int unigramFrequencyForThisWord = target.mFrequency;
             final int offset = addressOfBigram
                     - (mPosition + FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE);
-            int bigramFlags = BinaryDictEncoderUtils.makeBigramFlags(bigramIterator.hasNext(),
+            final int bigramFlags = BinaryDictEncoderUtils.makeBigramFlags(bigramIterator.hasNext(),
                     offset, bigram.mFrequency, unigramFrequencyForThisWord, bigram.mWord);
-            mBuffer[mPosition++] = (byte) bigramFlags;
+            mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, bigramFlags,
+                    FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE);
             mPosition += BinaryDictEncoderUtils.writeChildrenPosition(mBuffer, mPosition,
                     Math.abs(offset));
         }
@@ -230,8 +237,19 @@
 
     @Override
     public void writeForwardLinkAddress(final int forwardLinkAddress) {
-        mBuffer[mPosition++] = (byte) ((forwardLinkAddress >> 16) & 0xFF);
-        mBuffer[mPosition++] = (byte) ((forwardLinkAddress >> 8) & 0xFF);
-        mBuffer[mPosition++] = (byte) (forwardLinkAddress & 0xFF);
+        mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, forwardLinkAddress,
+                FormatSpec.FORWARD_LINK_ADDRESS_SIZE);
+    }
+
+    @Override
+    public void writePtNode(final PtNode ptNode, final int parentPosition,
+            final FormatOptions formatOptions, final FusionDictionary dict) {
+        writePtNodeFlags(ptNode, parentPosition, formatOptions);
+        writeParentPosition(parentPosition, ptNode, formatOptions);
+        writeCharacters(ptNode.mChars, ptNode.hasSeveralChars());
+        writeFrequency(ptNode.mFrequency);
+        writeChildrenPosition(ptNode, formatOptions);
+        writeShortcuts(ptNode.mShortcutTargets);
+        writeBigrams(ptNode.mBigrams, dict);
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
new file mode 100644
index 0000000..75b75ae
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
@@ -0,0 +1,269 @@
+/*
+/*
+ * 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.
+ */
+
+package com.android.inputmethod.latin.makedict;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding;
+import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
+import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
+import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions;
+import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode;
+import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray;
+import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+
+/**
+ * An implementation of DictEncoder for version 4 binary dictionary.
+ */
+@UsedForTesting
+public class Ver4DictEncoder implements DictEncoder {
+    private final File mDictPlacedDir;
+    private byte[] mTrieBuf;
+    private byte[] mFreqBuf;
+    private int mTriePos;
+    private OutputStream mTrieOutStream;
+    private OutputStream mFreqOutStream;
+
+    @UsedForTesting
+    public Ver4DictEncoder(final File dictPlacedDir) {
+        mDictPlacedDir = dictPlacedDir;
+    }
+
+    private void openStreams(final FormatOptions formatOptions, final DictionaryOptions dictOptions)
+            throws FileNotFoundException, IOException {
+        final FileHeader header = new FileHeader(0, dictOptions, formatOptions);
+        final String filename = header.getId() + "." + header.getVersion();
+        final File mDictDir = new File(mDictPlacedDir, filename);
+        final File trieFile = new File(mDictDir, filename + FormatSpec.TRIE_FILE_EXTENSION);
+        final File freqFile = new File(mDictDir, filename + FormatSpec.FREQ_FILE_EXTENSION);
+        if (!mDictDir.isDirectory()) {
+            if (mDictDir.exists()) mDictDir.delete();
+            mDictDir.mkdirs();
+        }
+        if (!trieFile.exists()) trieFile.createNewFile();
+        if (!freqFile.exists()) freqFile.createNewFile();
+        mTrieOutStream = new FileOutputStream(trieFile);
+        mFreqOutStream = new FileOutputStream(freqFile);
+    }
+
+    private void close() throws IOException {
+        try {
+            if (mTrieOutStream != null) {
+                mTrieOutStream.close();
+            }
+            if (mFreqOutStream != null) {
+                mFreqOutStream.close();
+            }
+        } finally {
+            mTrieOutStream = null;
+            mFreqOutStream = null;
+        }
+    }
+
+    @Override
+    public void writeDictionary(final FusionDictionary dict, final FormatOptions formatOptions)
+            throws IOException, UnsupportedFormatException {
+        if (formatOptions.mVersion != FormatSpec.VERSION4) {
+            throw new UnsupportedFormatException("File header has a wrong version number : "
+                    + formatOptions.mVersion);
+        }
+        if (!mDictPlacedDir.isDirectory()) {
+            throw new UnsupportedFormatException("Given path is not a directory.");
+        }
+
+        if (mTrieOutStream == null) {
+            openStreams(formatOptions, dict.mOptions);
+        }
+
+        BinaryDictEncoderUtils.writeDictionaryHeader(mTrieOutStream, dict, formatOptions);
+
+        MakedictLog.i("Flattening the tree...");
+        ArrayList<PtNodeArray> flatNodes = BinaryDictEncoderUtils.flattenTree(dict.mRootNodeArray);
+        int terminalCount = 0;
+        for (final PtNodeArray array : flatNodes) {
+            for (final PtNode node : array.mData) {
+                if (node.isTerminal()) node.mTerminalId = terminalCount++;
+            }
+        }
+
+        MakedictLog.i("Computing addresses...");
+        BinaryDictEncoderUtils.computeAddresses(dict, flatNodes, formatOptions);
+        if (MakedictLog.DBG) BinaryDictEncoderUtils.checkFlatPtNodeArrayList(flatNodes);
+
+        final PtNodeArray lastNodeArray = flatNodes.get(flatNodes.size() - 1);
+        final int bufferSize = lastNodeArray.mCachedAddressAfterUpdate + lastNodeArray.mCachedSize;
+        mTrieBuf = new byte[bufferSize];
+        mFreqBuf = new byte[terminalCount * FormatSpec.FREQUENCY_AND_FLAGS_SIZE];
+
+        MakedictLog.i("Writing file...");
+        for (PtNodeArray nodeArray : flatNodes) {
+            BinaryDictEncoderUtils.writePlacedPtNodeArray(dict, this, nodeArray, formatOptions);
+        }
+        if (MakedictLog.DBG) {
+            BinaryDictEncoderUtils.showStatistics(flatNodes);
+            MakedictLog.i("has " + terminalCount + " terminals.");
+        }
+        mTrieOutStream.write(mTrieBuf);
+        mFreqOutStream.write(mFreqBuf);
+
+        MakedictLog.i("Done");
+        close();
+    }
+
+    @Override
+    public void setPosition(int position) {
+        if (mTrieBuf == null || position < 0 || position >- mTrieBuf.length) return;
+        mTriePos = position;
+    }
+
+    @Override
+    public int getPosition() {
+        return mTriePos;
+    }
+
+    @Override
+    public void writePtNodeCount(int ptNodeCount) {
+        final int countSize = BinaryDictIOUtils.getPtNodeCountSize(ptNodeCount);
+        // ptNodeCount must fit on one byte or two bytes.
+        // Please see comments in FormatSpec
+        if (countSize != 1 && countSize != 2) {
+            throw new RuntimeException("Strange size from getPtNodeCountSize : " + countSize);
+        }
+        mTriePos = BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, mTriePos, ptNodeCount,
+                countSize);
+    }
+
+    private void writePtNodeFlags(final PtNode ptNode, final int parentAddress,
+            final FormatOptions formatOptions) {
+        final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, formatOptions);
+        mTriePos = BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, mTriePos,
+                BinaryDictEncoderUtils.makePtNodeFlags(ptNode, mTriePos, childrenPos,
+                        formatOptions),
+                FormatSpec.PTNODE_FLAGS_SIZE);
+    }
+
+    private void writeParentPosition(int parentPos, final PtNode ptNode,
+            final FormatOptions formatOptions) {
+        if (parentPos != FormatSpec.NO_PARENT_ADDRESS) {
+            parentPos -= ptNode.mCachedAddressAfterUpdate;
+        }
+        mTriePos = BinaryDictEncoderUtils.writeParentAddress(mTrieBuf, mTriePos, parentPos,
+                formatOptions);
+    }
+
+    private void writeCharacters(final int[] characters, final boolean hasSeveralChars) {
+        mTriePos = CharEncoding.writeCharArray(characters, mTrieBuf, mTriePos);
+        if (hasSeveralChars) {
+            mTrieBuf[mTriePos++] = FormatSpec.PTNODE_CHARACTERS_TERMINATOR;
+        }
+    }
+
+    private void writeTerminalId(final int terminalId) {
+        mTriePos = BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, mTriePos, terminalId,
+                FormatSpec.PTNODE_TERMINAL_ID_SIZE);
+    }
+
+    private void writeFrequency(final int frequency, final int terminalId) {
+        final int freqPos = terminalId * FormatSpec.FREQUENCY_AND_FLAGS_SIZE;
+        BinaryDictEncoderUtils.writeUIntToBuffer(mFreqBuf, freqPos, frequency,
+                FormatSpec.FREQUENCY_AND_FLAGS_SIZE);
+    }
+
+    private void writeChildrenPosition(PtNode ptNode, FormatOptions formatOptions) {
+        final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, formatOptions);
+        if (formatOptions.mSupportsDynamicUpdate) {
+            mTriePos += BinaryDictEncoderUtils.writeSignedChildrenPosition(mTrieBuf,
+                    mTriePos, childrenPos);
+        } else {
+            mTriePos += BinaryDictEncoderUtils.writeChildrenPosition(mTrieBuf,
+                    mTriePos, childrenPos);
+        }
+    }
+
+    private void writeShortcuts(ArrayList<WeightedString> shortcuts) {
+        if (null == shortcuts || shortcuts.isEmpty()) return;
+
+        final int indexOfShortcutByteSize = mTriePos;
+        mTriePos += FormatSpec.PTNODE_SHORTCUT_LIST_SIZE_SIZE;
+        final Iterator<WeightedString> shortcutIterator = shortcuts.iterator();
+        while (shortcutIterator.hasNext()) {
+            final WeightedString target = shortcutIterator.next();
+            final int shortcutFlags = BinaryDictEncoderUtils.makeShortcutFlags(
+                    shortcutIterator.hasNext(),
+                    target.mFrequency);
+            mTrieBuf[mTriePos++] = (byte)shortcutFlags;
+            final int shortcutShift = CharEncoding.writeString(mTrieBuf, mTriePos,
+                    target.mWord);
+            mTriePos += shortcutShift;
+        }
+        final int shortcutByteSize = mTriePos - indexOfShortcutByteSize;
+        if (shortcutByteSize > FormatSpec.MAX_SHORTCUT_LIST_SIZE_IN_A_PTNODE) {
+            throw new RuntimeException("Shortcut list too large : " + shortcutByteSize);
+        }
+        BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, indexOfShortcutByteSize,
+                shortcutByteSize, FormatSpec.PTNODE_SHORTCUT_LIST_SIZE_SIZE);
+    }
+
+    private void writeBigrams(ArrayList<WeightedString> bigrams, FusionDictionary dict) {
+        if (bigrams == null) return;
+
+        final Iterator<WeightedString> bigramIterator = bigrams.iterator();
+        while (bigramIterator.hasNext()) {
+            final WeightedString bigram = bigramIterator.next();
+            final PtNode target =
+                    FusionDictionary.findWordInTree(dict.mRootNodeArray, bigram.mWord);
+            final int addressOfBigram = target.mCachedAddressAfterUpdate;
+            final int unigramFrequencyForThisWord = target.mFrequency;
+            final int offset = addressOfBigram
+                    - (mTriePos + FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE);
+            int bigramFlags = BinaryDictEncoderUtils.makeBigramFlags(bigramIterator.hasNext(),
+                    offset, bigram.mFrequency, unigramFrequencyForThisWord, bigram.mWord);
+            mTrieBuf[mTriePos++] = (byte) bigramFlags;
+            mTriePos += BinaryDictEncoderUtils.writeChildrenPosition(mTrieBuf,
+                    mTriePos, Math.abs(offset));
+        }
+    }
+
+    @Override
+    public void writeForwardLinkAddress(int forwardLinkAddress) {
+        mTriePos = BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, mTriePos,
+                forwardLinkAddress, FormatSpec.FORWARD_LINK_ADDRESS_SIZE);
+    }
+
+    @Override
+    public void writePtNode(final PtNode ptNode, final int parentPosition,
+            final FormatOptions formatOptions, final FusionDictionary dict) {
+        writePtNodeFlags(ptNode, parentPosition, formatOptions);
+        writeParentPosition(parentPosition, ptNode, formatOptions);
+        writeCharacters(ptNode.mChars, ptNode.hasSeveralChars());
+        if (ptNode.isTerminal()) {
+            writeTerminalId(ptNode.mTerminalId);
+            writeFrequency(ptNode.mFrequency, ptNode.mTerminalId);
+        }
+        writeChildrenPosition(ptNode, formatOptions);
+        writeShortcuts(ptNode.mShortcutTargets);
+        writeBigrams(ptNode.mBigrams, dict);
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/personalization/DynamicPredictionDictionaryBase.java b/java/src/com/android/inputmethod/latin/personalization/DynamicPredictionDictionaryBase.java
index 5b1d064..9364fb0 100644
--- a/java/src/com/android/inputmethod/latin/personalization/DynamicPredictionDictionaryBase.java
+++ b/java/src/com/android/inputmethod/latin/personalization/DynamicPredictionDictionaryBase.java
@@ -25,14 +25,13 @@
 import com.android.inputmethod.latin.ExpandableBinaryDictionary;
 import com.android.inputmethod.latin.LatinImeLogger;
 import com.android.inputmethod.latin.makedict.DictDecoder;
-import com.android.inputmethod.latin.makedict.Ver3DictDecoder;
+import com.android.inputmethod.latin.makedict.FormatSpec;
 import com.android.inputmethod.latin.settings.Settings;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 import com.android.inputmethod.latin.utils.UserHistoryDictIOUtils;
 import com.android.inputmethod.latin.utils.UserHistoryDictIOUtils.OnAddWordListener;
 
 import java.io.File;
-import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.util.ArrayList;
 
@@ -173,16 +172,19 @@
 
         // Load the dictionary from binary file
         final File dictFile = new File(mContext.getFilesDir(), mFileName);
-        final Ver3DictDecoder dictDecoder = new Ver3DictDecoder(dictFile,
+        final DictDecoder dictDecoder = FormatSpec.getDictDecoder(dictFile,
                 DictDecoder.USE_BYTEARRAY);
+        if (dictDecoder == null) {
+            // This is an expected condition: we don't have a user history dictionary for this
+            // language yet. It will be created sometime later.
+            return;
+        }
+
         try {
             dictDecoder.openDictBuffer();
             UserHistoryDictIOUtils.readDictionaryBinary(dictDecoder, listener);
-        } catch (FileNotFoundException e) {
-            // This is an expected condition: we don't have a user history dictionary for this
-            // language yet. It will be created sometime later.
         } catch (IOException e) {
-            Log.e(TAG, "IOException on opening a bytebuffer", e);
+            Log.d(TAG, "IOException on opening a bytebuffer", e);
         } finally {
             if (PROFILE_SAVE_RESTORE) {
                 final long diff = System.currentTimeMillis() - now;
diff --git a/java/src/com/android/inputmethod/latin/settings/Settings.java b/java/src/com/android/inputmethod/latin/settings/Settings.java
index 8732a59..1a0fecc 100644
--- a/java/src/com/android/inputmethod/latin/settings/Settings.java
+++ b/java/src/com/android/inputmethod/latin/settings/Settings.java
@@ -81,6 +81,7 @@
     public static final String PREF_GESTURE_FLOATING_PREVIEW_TEXT =
             "pref_gesture_floating_preview_text";
     public static final String PREF_SHOW_SETUP_WIZARD_ICON = "pref_show_setup_wizard_icon";
+    public static final String PREF_PHRASE_GESTURE_ENABLED = "pref_gesture_space_aware";
 
     public static final String PREF_INPUT_LANGUAGE = "input_language";
     public static final String PREF_SELECTED_LANGUAGES = "selected_languages";
@@ -97,6 +98,10 @@
     public static final String PREF_SEND_FEEDBACK = "send_feedback";
     public static final String PREF_ABOUT_KEYBOARD = "about_keyboard";
 
+    // Emoji
+    public static final String PREF_EMOJI_RECENT_KEYS = "emoji_recent_keys";
+    public static final String PREF_EMOJI_CATEGORY_LAST_TYPED_ID = "emoji_category_last_typed_id";
+
     private Resources mRes;
     private SharedPreferences mPrefs;
     private SettingsValues mSettingsValues;
@@ -216,6 +221,12 @@
                 && prefs.getBoolean(Settings.PREF_GESTURE_INPUT, true);
     }
 
+    public static boolean readPhraseGestureEnabled(final SharedPreferences prefs,
+            final Resources res) {
+        return prefs.getBoolean(Settings.PREF_PHRASE_GESTURE_ENABLED,
+                res.getBoolean(R.bool.config_default_phrase_gesture_enabled));
+    }
+
     public static boolean readFromBuildConfigIfToShowKeyPreviewPopupSettingsOption(
             final Resources res) {
         return res.getBoolean(R.bool.config_enable_show_option_of_key_preview_popup);
@@ -363,4 +374,24 @@
         final String tokenStr = mPrefs.getString(PREF_LAST_USED_PERSONALIZATION_TOKEN, null);
         return StringUtils.hexStringToByteArray(tokenStr);
     }
+
+    public static void writeEmojiRecentKeys(final SharedPreferences prefs, String str) {
+        prefs.edit().putString(PREF_EMOJI_RECENT_KEYS, str).apply();
+    }
+
+    public static String readEmojiRecentKeys(final SharedPreferences prefs) {
+        return prefs.getString(PREF_EMOJI_RECENT_KEYS, "");
+    }
+
+    public static void writeEmojiCategoryLastTypedId(
+            final SharedPreferences prefs, final int category, final int id) {
+        final String key = PREF_EMOJI_CATEGORY_LAST_TYPED_ID + category;
+        prefs.edit().putInt(key, id).apply();
+    }
+
+    public static int readEmojiCategoryLastTypedId(
+            final SharedPreferences prefs, final int category) {
+        final String key = PREF_EMOJI_CATEGORY_LAST_TYPED_ID + category;
+        return prefs.getInt(key, 0);
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
index 072bb87..ee322e9 100644
--- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
@@ -76,6 +76,7 @@
     public final boolean mGestureTrailEnabled;
     public final boolean mGestureFloatingPreviewTextEnabled;
     public final boolean mSlidingKeyInputPreviewEnabled;
+    public final boolean mPhraseGestureEnabled;
     public final int mKeyLongpressTimeout;
     public final Locale mLocale;
 
@@ -159,6 +160,7 @@
         mGestureTrailEnabled = prefs.getBoolean(Settings.PREF_GESTURE_PREVIEW_TRAIL, true);
         mGestureFloatingPreviewTextEnabled = prefs.getBoolean(
                 Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT, true);
+        mPhraseGestureEnabled = Settings.readPhraseGestureEnabled(prefs, res);
         mCorrectionEnabled = mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect;
         final String showSuggestionsSetting = prefs.getString(
                 Settings.PREF_SHOW_SUGGESTIONS_SETTING,
@@ -211,6 +213,7 @@
         mGestureInputEnabled = true;
         mGestureTrailEnabled = true;
         mGestureFloatingPreviewTextEnabled = true;
+        mPhraseGestureEnabled = true;
         mCorrectionEnabled = mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect;
         mSuggestionVisibility = 0;
         mIsInternal = false;
@@ -295,7 +298,8 @@
                 puncList.add(new SuggestedWordInfo(KeySpecParser.getLabel(puncSpec),
                         SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_HARDCODED,
                         Dictionary.DICTIONARY_HARDCODED,
-                        SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */));
+                        SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+                        SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
             }
         }
         return new SuggestedWords(puncList,
diff --git a/java/src/com/android/inputmethod/latin/utils/PositionalInfoForUserDictPendingAddition.java b/java/src/com/android/inputmethod/latin/utils/PositionalInfoForUserDictPendingAddition.java
deleted file mode 100644
index 1fc7ecc..0000000
--- a/java/src/com/android/inputmethod/latin/utils/PositionalInfoForUserDictPendingAddition.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * 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.utils;
-
-import android.view.inputmethod.EditorInfo;
-
-import com.android.inputmethod.latin.RichInputConnection;
-
-import java.util.Locale;
-
-/**
- * Holder class for data about a word already committed but that may still be edited.
- *
- * When the user chooses to add a word to the user dictionary by pressing the appropriate
- * suggestion, a dialog is presented to give a chance to edit the word before it is actually
- * registered as a user dictionary word. If the word is actually modified, the IME needs to
- * go back and replace the word that was committed with the amended version.
- * The word we need to replace with will only be known after it's actually committed, so
- * the IME needs to take a note of what it has to replace and where it is.
- * This class encapsulates this data.
- */
-public final class PositionalInfoForUserDictPendingAddition {
-    final private String mOriginalWord;
-    final private int mCursorPos; // Position of the cursor after the word
-    final private EditorInfo mEditorInfo; // On what binding this has been added
-    final private int mCapitalizedMode;
-    private String mActualWordBeingAdded;
-
-    public PositionalInfoForUserDictPendingAddition(final String word, final int cursorPos,
-            final EditorInfo editorInfo, final int capitalizedMode) {
-        mOriginalWord = word;
-        mCursorPos = cursorPos;
-        mEditorInfo = editorInfo;
-        mCapitalizedMode = capitalizedMode;
-    }
-
-    public void setActualWordBeingAdded(final String actualWordBeingAdded) {
-        mActualWordBeingAdded = actualWordBeingAdded;
-    }
-
-    /**
-     * Try to replace the string at the remembered position with the actual word being added.
-     *
-     * After the user validated the word being added, the IME has to replace the old version
-     * (which has been committed in the text view) with the amended version if it's different.
-     * This method tries to do that, but may fail because the IME is not yet ready to do so -
-     * for example, it is still waiting for the new string, or it is waiting to return to the text
-     * view in which the amendment should be made. In these cases, we should keep the data
-     * and wait until all conditions are met.
-     * This method returns true if the replacement has been successfully made and this data
-     * can be forgotten; it returns false if the replacement can't be made yet and we need to
-     * keep this until a later time.
-     * The IME knows about the actual word being added through a callback called by the
-     * user dictionary facility of the device. When this callback comes, the keyboard may still
-     * be connected to the edition dialog, or it may have already returned to the original text
-     * field. Replacement has to work in both cases.
-     * Accordingly, this method is called at two different points in time : upon getting the
-     * event that a new word was added to the user dictionary, and upon starting up in a
-     * new text field.
-     * @param connection The RichInputConnection through which to contact the editor.
-     * @param editorInfo Information pertaining to the editor we are currently in.
-     * @param currentCursorPosition The current cursor position, for checking purposes.
-     * @param locale The locale for changing case, if necessary
-     * @return true if the edit has been successfully made, false if we need to try again later
-     */
-    public boolean tryReplaceWithActualWord(final RichInputConnection connection,
-            final EditorInfo editorInfo, final int currentCursorPosition, final Locale locale) {
-        // If we still don't know the actual word being added, we need to try again later.
-        if (null == mActualWordBeingAdded) return false;
-        // The entered text and the registered text were the same anyway : we can
-        // return success right away even if focus has not returned yet to the text field we
-        // want to amend.
-        if (mActualWordBeingAdded.equals(mOriginalWord)) return true;
-        // Not the same text field : we need to try again later. This happens when the addition
-        // is reported by the user dictionary provider before the focus has moved back to the
-        // original text view, so the IME is still in the text view of the dialog and has no way to
-        // edit the original text view at this time.
-        if (!mEditorInfo.packageName.equals(editorInfo.packageName)
-                || mEditorInfo.fieldId != editorInfo.fieldId) {
-            return false;
-        }
-        // Same text field, but not the same cursor position : we give up, so we return success
-        // so that it won't be tried again
-        if (currentCursorPosition != mCursorPos) return true;
-        // We have made all the checks : do the replacement and report success
-        // If this was auto-capitalized, we need to restore the case before committing
-        final String wordWithCaseFixed = CapsModeUtils.applyAutoCapsMode(mActualWordBeingAdded,
-                mCapitalizedMode, locale);
-        connection.setComposingRegion(currentCursorPosition - mOriginalWord.length(),
-                currentCursorPosition);
-        connection.commitText(wordWithCaseFixed, wordWithCaseFixed.length());
-        return true;
-    }
-}
diff --git a/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java b/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java
index 3c1db65..5dc0b58 100644
--- a/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java
+++ b/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java
@@ -31,6 +31,7 @@
     private static final int TASK_QUEUE_CAPACITY = 1000;
     private final Queue<Runnable> mTasks;
     private final Queue<Runnable> mPrioritizedTasks;
+    private boolean mIsShutdown;
 
     // The task which is running now.
     private Runnable mActive;
@@ -38,6 +39,7 @@
     public PrioritizedSerialExecutor() {
         mTasks = new ArrayDeque<Runnable>(TASK_QUEUE_CAPACITY);
         mPrioritizedTasks = new ArrayDeque<Runnable>(TASK_QUEUE_CAPACITY);
+        mIsShutdown = false;
     }
 
     /**
@@ -56,9 +58,11 @@
      */
     public void execute(final Runnable r) {
         synchronized(mLock) {
-            mTasks.offer(r);
-            if (mActive == null) {
-                scheduleNext();
+            if (!mIsShutdown) {
+                mTasks.offer(r);
+                if (mActive == null) {
+                    scheduleNext();
+                }
             }
         }
     }
@@ -69,9 +73,11 @@
      */
     public void executePrioritized(final Runnable r) {
         synchronized(mLock) {
-            mPrioritizedTasks.offer(r);
-            if (mActive ==  null) {
-                scheduleNext();
+            if (!mIsShutdown) {
+                mPrioritizedTasks.offer(r);
+                if (mActive ==  null) {
+                    scheduleNext();
+                }
             }
         }
     }
@@ -123,4 +129,19 @@
             execute(newTask);
         }
     }
+
+    public void shutdown() {
+        synchronized(mLock) {
+            mIsShutdown = true;
+        }
+    }
+
+    public boolean isTerminated() {
+        synchronized(mLock) {
+            if (!mIsShutdown) {
+                return false;
+            }
+            return mPrioritizedTasks.isEmpty() && mTasks.isEmpty() && mActive == null;
+        }
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java b/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java
index 4c7739a..7c6fe93 100644
--- a/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java
+++ b/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java
@@ -132,6 +132,15 @@
         }
     }
 
+    /**
+     * Shift to the left by elementCount, discarding elementCount pointers at the start.
+     * @param elementCount how many elements to shift.
+     */
+    public void shift(final int elementCount) {
+        System.arraycopy(mArray, elementCount, mArray, 0, mLength - elementCount);
+        mLength -= elementCount;
+    }
+
     @Override
     public String toString() {
         final StringBuilder sb = new StringBuilder();
diff --git a/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java b/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java
index 05f3061..ea32a74 100644
--- a/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java
@@ -20,13 +20,13 @@
 
 import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.makedict.BinaryDictIOUtils;
+import com.android.inputmethod.latin.makedict.DictDecoder;
 import com.android.inputmethod.latin.makedict.DictEncoder;
 import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
 import com.android.inputmethod.latin.makedict.FusionDictionary;
 import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray;
 import com.android.inputmethod.latin.makedict.PendingAttribute;
 import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
-import com.android.inputmethod.latin.makedict.Ver3DictDecoder;
 import com.android.inputmethod.latin.personalization.UserHistoryDictionaryBigramList;
 
 import java.io.IOException;
@@ -125,7 +125,7 @@
     /**
      * Reads dictionary from file.
      */
-    public static void readDictionaryBinary(final Ver3DictDecoder dictDecoder,
+    public static void readDictionaryBinary(final DictDecoder dictDecoder,
             final OnAddWordListener dict) {
         final TreeMap<Integer, String> unigrams = CollectionUtils.newTreeMap();
         final TreeMap<Integer, Integer> frequencies = CollectionUtils.newTreeMap();
diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
index 8da1859..a63fab6 100644
--- a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
+++ b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
@@ -46,8 +46,7 @@
     sourceDirChars[sourceDirUtf8Length] = '\0';
     DictionaryStructureWithBufferPolicy *const dictionaryStructureWithBufferPolicy =
             DictionaryStructureWithBufferPolicyFactory::newDictionaryStructureWithBufferPolicy(
-                    sourceDirChars, static_cast<int>(sourceDirUtf8Length),
-                    static_cast<int>(dictOffset), static_cast<int>(dictSize),
+                    sourceDirChars, static_cast<int>(dictOffset), static_cast<int>(dictSize),
                     isUpdatable == JNI_TRUE);
     if (!dictionaryStructureWithBufferPolicy) {
         return 0;
@@ -59,6 +58,35 @@
     return reinterpret_cast<jlong>(dictionary);
 }
 
+static void latinime_BinaryDictionary_flush(JNIEnv *env, jclass clazz, jlong dict,
+        jstring filePath) {
+    Dictionary *dictionary = reinterpret_cast<Dictionary *>(dict);
+    if (!dictionary) return;
+    const jsize filePathUtf8Length = env->GetStringUTFLength(filePath);
+    char filePathChars[filePathUtf8Length + 1];
+    env->GetStringUTFRegion(filePath, 0, env->GetStringLength(filePath), filePathChars);
+    filePathChars[filePathUtf8Length] = '\0';
+    dictionary->flush(filePathChars);
+}
+
+static bool latinime_BinaryDictionary_needsToRunGC(JNIEnv *env, jclass clazz,
+        jlong dict) {
+    Dictionary *dictionary = reinterpret_cast<Dictionary *>(dict);
+    if (!dictionary) return false;
+    return dictionary->needsToRunGC();
+}
+
+static void latinime_BinaryDictionary_flushWithGC(JNIEnv *env, jclass clazz, jlong dict,
+        jstring filePath) {
+    Dictionary *dictionary = reinterpret_cast<Dictionary *>(dict);
+    if (!dictionary) return;
+    const jsize filePathUtf8Length = env->GetStringUTFLength(filePath);
+    char filePathChars[filePathUtf8Length + 1];
+    env->GetStringUTFRegion(filePath, 0, env->GetStringLength(filePath), filePathChars);
+    filePathChars[filePathUtf8Length] = '\0';
+    dictionary->flushWithGC(filePathChars);
+}
+
 static void latinime_BinaryDictionary_close(JNIEnv *env, jclass clazz, jlong dict) {
     Dictionary *dictionary = reinterpret_cast<Dictionary *>(dict);
     if (!dictionary) return;
@@ -70,7 +98,8 @@
         jintArray yCoordinatesArray, jintArray timesArray, jintArray pointerIdsArray,
         jintArray inputCodePointsArray, jint inputSize, jint commitPoint, jintArray suggestOptions,
         jintArray prevWordCodePointsForBigrams, jintArray outputCodePointsArray,
-        jintArray scoresArray, jintArray spaceIndicesArray, jintArray outputTypesArray) {
+        jintArray scoresArray, jintArray spaceIndicesArray, jintArray outputTypesArray,
+        jintArray outputAutoCommitFirstWordConfidence) {
     Dictionary *dictionary = reinterpret_cast<Dictionary *>(dict);
     if (!dictionary) return 0;
     ProximityInfo *pInfo = reinterpret_cast<ProximityInfo *>(proximityInfo);
@@ -252,8 +281,23 @@
         reinterpret_cast<void *>(latinime_BinaryDictionary_close)
     },
     {
+        const_cast<char *>("flushNative"),
+        const_cast<char *>("(JLjava/lang/String;)V"),
+        reinterpret_cast<void *>(latinime_BinaryDictionary_flush)
+    },
+    {
+        const_cast<char *>("needsToRunGCNative"),
+        const_cast<char *>("(J)Z"),
+        reinterpret_cast<void *>(latinime_BinaryDictionary_needsToRunGC)
+    },
+    {
+        const_cast<char *>("flushWithGCNative"),
+        const_cast<char *>("(JLjava/lang/String;)V"),
+        reinterpret_cast<void *>(latinime_BinaryDictionary_flushWithGC)
+    },
+    {
         const_cast<char *>("getSuggestionsNative"),
-        const_cast<char *>("(JJJ[I[I[I[I[III[I[I[I[I[I[I)I"),
+        const_cast<char *>("(JJJ[I[I[I[I[III[I[I[I[I[I[I[I)I"),
         reinterpret_cast<void *>(latinime_BinaryDictionary_getSuggestions)
     },
     {
diff --git a/native/jni/com_android_inputmethod_latin_DicTraverseSession.cpp b/native/jni/com_android_inputmethod_latin_DicTraverseSession.cpp
index 72e6258..3866433 100644
--- a/native/jni/com_android_inputmethod_latin_DicTraverseSession.cpp
+++ b/native/jni/com_android_inputmethod_latin_DicTraverseSession.cpp
@@ -25,8 +25,9 @@
 
 namespace latinime {
 class Dictionary;
-static jlong latinime_setDicTraverseSession(JNIEnv *env, jclass clazz, jstring localeJStr) {
-    void *traverseSession = DicTraverseSession::getSessionInstance(env, localeJStr);
+static jlong latinime_setDicTraverseSession(JNIEnv *env, jclass clazz, jstring localeJStr,
+        jlong dictSize) {
+    void *traverseSession = DicTraverseSession::getSessionInstance(env, localeJStr, dictSize);
     return reinterpret_cast<jlong>(traverseSession);
 }
 
@@ -53,7 +54,7 @@
 static const JNINativeMethod sMethods[] = {
     {
         const_cast<char *>("setDicTraverseSessionNative"),
-        const_cast<char *>("(Ljava/lang/String;)J"),
+        const_cast<char *>("(Ljava/lang/String;J)J"),
         reinterpret_cast<void *>(latinime_setDicTraverseSession)
     },
     {
diff --git a/native/jni/src/defines.h b/native/jni/src/defines.h
index 34a646f..89dfa39 100644
--- a/native/jni/src/defines.h
+++ b/native/jni/src/defines.h
@@ -292,7 +292,6 @@
 // of the binary dictionary where a {key,value} string pair scheme is used.
 #define LARGEST_INT_DIGIT_COUNT 11
 
-#define NOT_A_VALID_WORD_POS (-99)
 #define NOT_A_CODE_POINT (-1)
 #define NOT_A_DISTANCE (-1)
 #define NOT_A_COORDINATE (-1)
@@ -322,13 +321,6 @@
 #define MAX_POINTER_COUNT 1
 #define MAX_POINTER_COUNT_G 2
 
-// Queue IDs and size for DicNodesCache
-#define DIC_NODES_CACHE_INITIAL_QUEUE_ID_ACTIVE 0
-#define DIC_NODES_CACHE_INITIAL_QUEUE_ID_NEXT_ACTIVE 1
-#define DIC_NODES_CACHE_INITIAL_QUEUE_ID_TERMINAL 2
-#define DIC_NODES_CACHE_INITIAL_QUEUE_ID_CACHE_FOR_CONTINUOUS_SUGGESTION 3
-#define DIC_NODES_CACHE_PRIORITY_QUEUES_SIZE 4
-
 template<typename T> AK_FORCE_INLINE const T &min(const T &a, const T &b) { return a < b ? a : b; }
 template<typename T> AK_FORCE_INLINE const T &max(const T &a, const T &b) { return a > b ? a : b; }
 
diff --git a/native/jni/src/suggest/core/dicnode/dic_node.h b/native/jni/src/suggest/core/dicnode/dic_node.h
index cdd9f59..41ef9d2 100644
--- a/native/jni/src/suggest/core/dicnode/dic_node.h
+++ b/native/jni/src/suggest/core/dicnode/dic_node.h
@@ -112,7 +112,7 @@
         mIsUsed = true;
         mIsCachedForNextSuggestion = false;
         mDicNodeProperties.init(
-                NOT_A_VALID_WORD_POS /* pos */, rootGroupPos, NOT_A_CODE_POINT /* nodeCodePoint */,
+                NOT_A_DICT_POS /* pos */, rootGroupPos, NOT_A_CODE_POINT /* nodeCodePoint */,
                 NOT_A_PROBABILITY /* probability */, false /* isTerminal */,
                 true /* hasChildren */, false /* isBlacklistedOrNotAWord */, 0 /* depth */,
                 0 /* terminalDepth */);
@@ -125,7 +125,7 @@
         mIsUsed = true;
         mIsCachedForNextSuggestion = dicNode->mIsCachedForNextSuggestion;
         mDicNodeProperties.init(
-                NOT_A_VALID_WORD_POS /* pos */, rootGroupPos, NOT_A_CODE_POINT /* nodeCodePoint */,
+                NOT_A_DICT_POS /* pos */, rootGroupPos, NOT_A_CODE_POINT /* nodeCodePoint */,
                 NOT_A_PROBABILITY /* probability */, false /* isTerminal */,
                 true /* hasChildren */, false /* isBlacklistedOrNotAWord */,  0 /* depth */,
                 0 /* terminalDepth */);
@@ -143,7 +143,7 @@
                 dicNode->mDicNodeState.mDicNodeStatePrevWord.getPrevWordLength(),
                 dicNode->getOutputWordBuf(),
                 dicNode->mDicNodeProperties.getDepth(),
-                dicNode->mDicNodeState.mDicNodeStatePrevWord.mPrevSpacePositions,
+                dicNode->mDicNodeState.mDicNodeStatePrevWord.getSecondWordFirstInputIndex(),
                 mDicNodeState.mDicNodeStateInput.getInputIndex(0) /* lastInputIndex */);
         PROF_NODE_COPY(&dicNode->mProfiler, mProfiler);
     }
@@ -234,7 +234,7 @@
     }
 
     bool isFirstWord() const {
-        return mDicNodeState.mDicNodeStatePrevWord.getPrevWordNodePos() == NOT_A_VALID_WORD_POS;
+        return mDicNodeState.mDicNodeStatePrevWord.getPrevWordNodePos() == NOT_A_DICT_POS;
     }
 
     bool isCompletion(const int inputSize) const {
@@ -321,8 +321,13 @@
         DUMP_WORD_AND_SCORE("OUTPUT");
     }
 
-    void outputSpacePositionsResult(int *spaceIndices) const {
-        mDicNodeState.mDicNodeStatePrevWord.outputSpacePositions(spaceIndices);
+    int getSecondWordFirstInputIndex(const ProximityInfoState *const pInfoState) const {
+        const int inputIndex = mDicNodeState.mDicNodeStatePrevWord.getSecondWordFirstInputIndex();
+        if (inputIndex == NOT_AN_INDEX) {
+            return NOT_AN_INDEX;
+        } else {
+            return pInfoState->getInputIndexOfSampledPoint(inputIndex);
+        }
     }
 
     bool hasMultipleWords() const {
@@ -573,7 +578,11 @@
         }
     }
 
-    AK_FORCE_INLINE void updateInputIndexG(DicNode_InputStateG *inputStateG) {
+    AK_FORCE_INLINE void updateInputIndexG(const DicNode_InputStateG *const inputStateG) {
+        if (mDicNodeState.mDicNodeStatePrevWord.getPrevWordCount() == 1 && isFirstLetter()) {
+            mDicNodeState.mDicNodeStatePrevWord.setSecondWordFirstInputIndex(
+                    inputStateG->mInputIndex);
+        }
         mDicNodeState.mDicNodeStateInput.updateInputIndexG(inputStateG->mPointerId,
                 inputStateG->mInputIndex, inputStateG->mPrevCodePoint,
                 inputStateG->mTerminalDiffCost, inputStateG->mRawLength);
diff --git a/native/jni/src/suggest/core/dicnode/dic_node_priority_queue.h b/native/jni/src/suggest/core/dicnode/dic_node_priority_queue.h
index 2a486b8..7461f0c 100644
--- a/native/jni/src/suggest/core/dicnode/dic_node_priority_queue.h
+++ b/native/jni/src/suggest/core/dicnode/dic_node_priority_queue.h
@@ -24,20 +24,16 @@
 #include "suggest/core/dicnode/dic_node.h"
 #include "suggest/core/dicnode/dic_node_release_listener.h"
 
-// The biggest value among MAX_CACHE_DIC_NODE_SIZE, MAX_CACHE_DIC_NODE_SIZE_FOR_SINGLE_POINT, ...
-#define MAX_DIC_NODE_PRIORITY_QUEUE_CAPACITY 310
-
 namespace latinime {
 
 class DicNodePriorityQueue : public DicNodeReleaseListener {
  public:
-    AK_FORCE_INLINE DicNodePriorityQueue()
-            : MAX_CAPACITY(MAX_DIC_NODE_PRIORITY_QUEUE_CAPACITY),
-              mMaxSize(MAX_DIC_NODE_PRIORITY_QUEUE_CAPACITY), mDicNodesBuf(), mUnusedNodeIndices(),
-              mNextUnusedNodeId(0), mDicNodesQueue() {
-        mDicNodesBuf.resize(MAX_CAPACITY + 1);
-        mUnusedNodeIndices.resize(MAX_CAPACITY + 1);
-        reset();
+    AK_FORCE_INLINE explicit DicNodePriorityQueue(const int capacity)
+            : mCapacity(capacity), mMaxSize(capacity), mDicNodesBuf(),
+              mUnusedNodeIndices(), mNextUnusedNodeId(0), mDicNodesQueue() {
+        mDicNodesBuf.resize(mCapacity + 1);
+        mUnusedNodeIndices.resize(mCapacity + 1);
+        clearAndResizeToCapacity();
     }
 
     // Non virtual inline destructor -- never inherit this class
@@ -52,11 +48,12 @@
     }
 
     AK_FORCE_INLINE void setMaxSize(const int maxSize) {
-        mMaxSize = min(maxSize, MAX_CAPACITY);
+        ASSERT(maxSize <= mCapacity);
+        mMaxSize = min(maxSize, mCapacity);
     }
 
-    AK_FORCE_INLINE void reset() {
-        clearAndResize(MAX_CAPACITY);
+    AK_FORCE_INLINE void clearAndResizeToCapacity() {
+        clearAndResize(mCapacity);
     }
 
     AK_FORCE_INLINE void clear() {
@@ -64,27 +61,19 @@
     }
 
     AK_FORCE_INLINE void clearAndResize(const int maxSize) {
+        ASSERT(maxSize <= mCapacity);
         while (!mDicNodesQueue.empty()) {
             mDicNodesQueue.pop();
         }
         setMaxSize(maxSize);
-        for (int i = 0; i < MAX_CAPACITY + 1; ++i) {
+        for (int i = 0; i < mCapacity + 1; ++i) {
             mDicNodesBuf[i].remove();
             mDicNodesBuf[i].setReleaseListener(this);
-            mUnusedNodeIndices[i] = i == MAX_CAPACITY ? NOT_A_NODE_ID : static_cast<int>(i) + 1;
+            mUnusedNodeIndices[i] = i == mCapacity ? NOT_A_NODE_ID : static_cast<int>(i) + 1;
         }
         mNextUnusedNodeId = 0;
     }
 
-    AK_FORCE_INLINE DicNode *newDicNode(DicNode *dicNode) {
-        DicNode *newNode = searchEmptyDicNode();
-        if (newNode) {
-            DicNodeUtils::initByCopy(dicNode, newNode);
-            return newNode;
-        }
-        return 0;
-    }
-
     // Copy
     AK_FORCE_INLINE DicNode *copyPush(DicNode *dicNode) {
         return copyPush(dicNode, mMaxSize);
@@ -111,12 +100,12 @@
         }
         mUnusedNodeIndices[index] = mNextUnusedNodeId;
         mNextUnusedNodeId = index;
-        ASSERT(index >= 0 && index < (MAX_CAPACITY + 1));
+        ASSERT(index >= 0 && index < (mCapacity + 1));
     }
 
     AK_FORCE_INLINE void dump() const {
         AKLOGI("\n\n\n\n\n===========================");
-        for (int i = 0; i < MAX_CAPACITY + 1; ++i) {
+        for (int i = 0; i < mCapacity + 1; ++i) {
             if (mDicNodesBuf[i].isUsed()) {
                 mDicNodesBuf[i].dump("QUEUE: ");
             }
@@ -125,7 +114,7 @@
     }
 
  private:
-    DISALLOW_COPY_AND_ASSIGN(DicNodePriorityQueue);
+    DISALLOW_IMPLICIT_CONSTRUCTORS(DicNodePriorityQueue);
     static const int NOT_A_NODE_ID = -1;
 
     AK_FORCE_INLINE static bool compareDicNode(DicNode *left, DicNode *right) {
@@ -139,7 +128,7 @@
     };
 
     typedef std::priority_queue<DicNode *, std::vector<DicNode *>, DicNodeComparator> DicNodesQueue;
-    const int MAX_CAPACITY;
+    const int mCapacity;
     int mMaxSize;
     std::vector<DicNode> mDicNodesBuf; // of each element of mDicNodesBuf respectively
     std::vector<int> mUnusedNodeIndices;
@@ -163,13 +152,12 @@
     }
 
     AK_FORCE_INLINE DicNode *searchEmptyDicNode() {
-        // TODO: Currently O(n) but should be improved to O(1)
-        if (MAX_CAPACITY == 0) {
+        if (mCapacity == 0) {
             return 0;
         }
         if (mNextUnusedNodeId == NOT_A_NODE_ID) {
             AKLOGI("No unused node found.");
-            for (int i = 0; i < MAX_CAPACITY + 1; ++i) {
+            for (int i = 0; i < mCapacity + 1; ++i) {
                 AKLOGI("Dump node availability, %d, %d, %d",
                         i, mDicNodesBuf[i].isUsed(), mUnusedNodeIndices[i]);
             }
@@ -185,7 +173,7 @@
         const int index = static_cast<int>(dicNode - &mDicNodesBuf[0]);
         mNextUnusedNodeId = mUnusedNodeIndices[index];
         mUnusedNodeIndices[index] = NOT_A_NODE_ID;
-        ASSERT(index >= 0 && index < (MAX_CAPACITY + 1));
+        ASSERT(index >= 0 && index < (mCapacity + 1));
     }
 
     AK_FORCE_INLINE DicNode *pushPoolNodeWithMaxSize(DicNode *dicNode, const int maxSize) {
@@ -209,6 +197,15 @@
     AK_FORCE_INLINE DicNode *copyPush(DicNode *dicNode, const int maxSize) {
         return pushPoolNodeWithMaxSize(newDicNode(dicNode), maxSize);
     }
+
+    AK_FORCE_INLINE DicNode *newDicNode(DicNode *dicNode) {
+        DicNode *newNode = searchEmptyDicNode();
+        if (newNode) {
+            DicNodeUtils::initByCopy(dicNode, newNode);
+        }
+        return newNode;
+    }
+
 };
 } // namespace latinime
 #endif // LATINIME_DIC_NODE_PRIORITY_QUEUE_H
diff --git a/native/jni/src/suggest/core/dicnode/dic_node_utils.cpp b/native/jni/src/suggest/core/dicnode/dic_node_utils.cpp
index e815919..ec65114 100644
--- a/native/jni/src/suggest/core/dicnode/dic_node_utils.cpp
+++ b/native/jni/src/suggest/core/dicnode/dic_node_utils.cpp
@@ -89,7 +89,7 @@
     const int unigramProbability = node->getProbability();
     const int wordPos = node->getPos();
     const int prevWordPos = node->getPrevWordPos();
-    if (NOT_A_VALID_WORD_POS == wordPos || NOT_A_VALID_WORD_POS == prevWordPos) {
+    if (NOT_A_DICT_POS == wordPos || NOT_A_DICT_POS == prevWordPos) {
         // Note: Normally wordPos comes from the dictionary and should never equal
         // NOT_A_VALID_WORD_POS.
         return dictionaryStructurePolicy->getProbability(unigramProbability,
diff --git a/native/jni/src/suggest/core/dicnode/dic_nodes_cache.cpp b/native/jni/src/suggest/core/dicnode/dic_nodes_cache.cpp
index c3d2a2e..b6be47e 100644
--- a/native/jni/src/suggest/core/dicnode/dic_nodes_cache.cpp
+++ b/native/jni/src/suggest/core/dicnode/dic_nodes_cache.cpp
@@ -23,6 +23,11 @@
 
 namespace latinime {
 
+// The biggest value among MAX_CACHE_DIC_NODE_SIZE, MAX_CACHE_DIC_NODE_SIZE_FOR_SINGLE_POINT, ...
+const int DicNodesCache::LARGE_PRIORITY_QUEUE_CAPACITY = 310;
+// Capacity for reducing memory footprint.
+const int DicNodesCache::SMALL_PRIORITY_QUEUE_CAPACITY = 100;
+
 /**
  * Truncates all of the dicNodes so that they start at the given commit point.
  * Only called for multi-word typing input.
diff --git a/native/jni/src/suggest/core/dicnode/dic_nodes_cache.h b/native/jni/src/suggest/core/dicnode/dic_nodes_cache.h
index 7aab090..8493b6a 100644
--- a/native/jni/src/suggest/core/dicnode/dic_nodes_cache.h
+++ b/native/jni/src/suggest/core/dicnode/dic_nodes_cache.h
@@ -31,25 +31,32 @@
  */
 class DicNodesCache {
  public:
-    AK_FORCE_INLINE DicNodesCache()
-            : mActiveDicNodes(&mDicNodePriorityQueues[DIC_NODES_CACHE_INITIAL_QUEUE_ID_ACTIVE]),
-              mNextActiveDicNodes(&mDicNodePriorityQueues[
-                      DIC_NODES_CACHE_INITIAL_QUEUE_ID_NEXT_ACTIVE]),
-              mTerminalDicNodes(&mDicNodePriorityQueues[DIC_NODES_CACHE_INITIAL_QUEUE_ID_TERMINAL]),
-              mCachedDicNodesForContinuousSuggestion(&mDicNodePriorityQueues[
-                      DIC_NODES_CACHE_INITIAL_QUEUE_ID_CACHE_FOR_CONTINUOUS_SUGGESTION]),
-              mInputIndex(0), mLastCachedInputIndex(0) {
-    }
+    AK_FORCE_INLINE explicit DicNodesCache(const bool usesLargeCapacityCache)
+            : mUsesLargeCapacityCache(usesLargeCapacityCache),
+              mDicNodePriorityQueue0(getCacheCapacity()),
+              mDicNodePriorityQueue1(getCacheCapacity()),
+              mDicNodePriorityQueue2(getCacheCapacity()),
+              mDicNodePriorityQueueForTerminal(MAX_RESULTS),
+              mActiveDicNodes(&mDicNodePriorityQueue0),
+              mNextActiveDicNodes(&mDicNodePriorityQueue1),
+              mCachedDicNodesForContinuousSuggestion(&mDicNodePriorityQueue2),
+              mTerminalDicNodes(&mDicNodePriorityQueueForTerminal),
+              mInputIndex(0), mLastCachedInputIndex(0) {}
 
     AK_FORCE_INLINE virtual ~DicNodesCache() {}
 
     AK_FORCE_INLINE void reset(const int nextActiveSize, const int terminalSize) {
         mInputIndex = 0;
         mLastCachedInputIndex = 0;
-        mActiveDicNodes->reset();
-        mNextActiveDicNodes->clearAndResize(nextActiveSize);
+        // We want to use the max capacity for the current active dic node queue.
+        mActiveDicNodes->clearAndResizeToCapacity();
+        // nextActiveSize is used to limit the next iteration's active dic node size.
+        const int nextActiveSizeFittingToTheCapacity = min(nextActiveSize, getCacheCapacity());
+        mNextActiveDicNodes->clearAndResize(nextActiveSizeFittingToTheCapacity);
         mTerminalDicNodes->clearAndResize(terminalSize);
-        mCachedDicNodesForContinuousSuggestion->reset();
+        // We want to use the max capacity for the cached dic nodes that will be used for the
+        // continuous suggestion.
+        mCachedDicNodesForContinuousSuggestion->clearAndResizeToCapacity();
     }
 
     AK_FORCE_INLINE void continueSearch() {
@@ -157,21 +164,35 @@
         return tmp;
     }
 
+    AK_FORCE_INLINE int getCacheCapacity() const {
+        return mUsesLargeCapacityCache ?
+                LARGE_PRIORITY_QUEUE_CAPACITY : SMALL_PRIORITY_QUEUE_CAPACITY;
+    }
+
     AK_FORCE_INLINE void resetTemporaryCaches() {
         mActiveDicNodes->clear();
         mNextActiveDicNodes->clear();
         mTerminalDicNodes->clear();
     }
 
-    DicNodePriorityQueue mDicNodePriorityQueues[DIC_NODES_CACHE_PRIORITY_QUEUES_SIZE];
+    static const int LARGE_PRIORITY_QUEUE_CAPACITY;
+    static const int SMALL_PRIORITY_QUEUE_CAPACITY;
+
+    const bool mUsesLargeCapacityCache;
+    // Instances
+    DicNodePriorityQueue mDicNodePriorityQueue0;
+    DicNodePriorityQueue mDicNodePriorityQueue1;
+    DicNodePriorityQueue mDicNodePriorityQueue2;
+    DicNodePriorityQueue mDicNodePriorityQueueForTerminal;
+
     // Active dicNodes currently being expanded.
     DicNodePriorityQueue *mActiveDicNodes;
     // Next dicNodes to be expanded.
     DicNodePriorityQueue *mNextActiveDicNodes;
-    // Current top terminal dicNodes.
-    DicNodePriorityQueue *mTerminalDicNodes;
     // Cached dicNodes used for continuous suggestion.
     DicNodePriorityQueue *mCachedDicNodesForContinuousSuggestion;
+    // Current top terminal dicNodes.
+    DicNodePriorityQueue *mTerminalDicNodes;
     int mInputIndex;
     int mLastCachedInputIndex;
 };
diff --git a/native/jni/src/suggest/core/dicnode/internal/dic_node_state_prevword.h b/native/jni/src/suggest/core/dicnode/internal/dic_node_state_prevword.h
index 9bc9687..b898620 100644
--- a/native/jni/src/suggest/core/dicnode/internal/dic_node_state_prevword.h
+++ b/native/jni/src/suggest/core/dicnode/internal/dic_node_state_prevword.h
@@ -22,6 +22,7 @@
 
 #include "defines.h"
 #include "suggest/core/dicnode/dic_node_utils.h"
+#include "suggest/core/layout/proximity_info_state.h"
 
 namespace latinime {
 
@@ -29,9 +30,8 @@
  public:
     AK_FORCE_INLINE DicNodeStatePrevWord()
             : mPrevWordCount(0), mPrevWordLength(0), mPrevWordStart(0), mPrevWordProbability(0),
-              mPrevWordNodePos(NOT_A_VALID_WORD_POS) {
+              mPrevWordNodePos(NOT_A_DICT_POS), mSecondWordFirstInputIndex(NOT_AN_INDEX) {
         memset(mPrevWord, 0, sizeof(mPrevWord));
-        memset(mPrevSpacePositions, 0, sizeof(mPrevSpacePositions));
     }
 
     virtual ~DicNodeStatePrevWord() {}
@@ -41,8 +41,8 @@
         mPrevWordCount = 0;
         mPrevWordStart = 0;
         mPrevWordProbability = -1;
-        mPrevWordNodePos = NOT_A_VALID_WORD_POS;
-        memset(mPrevSpacePositions, 0, sizeof(mPrevSpacePositions));
+        mPrevWordNodePos = NOT_A_DICT_POS;
+        mSecondWordFirstInputIndex = NOT_AN_INDEX;
     }
 
     void init(const int prevWordNodePos) {
@@ -51,7 +51,7 @@
         mPrevWordStart = 0;
         mPrevWordProbability = -1;
         mPrevWordNodePos = prevWordNodePos;
-        memset(mPrevSpacePositions, 0, sizeof(mPrevSpacePositions));
+        mSecondWordFirstInputIndex = NOT_AN_INDEX;
     }
 
     // Init by copy
@@ -61,14 +61,14 @@
         mPrevWordStart = prevWord->mPrevWordStart;
         mPrevWordProbability = prevWord->mPrevWordProbability;
         mPrevWordNodePos = prevWord->mPrevWordNodePos;
+        mSecondWordFirstInputIndex = prevWord->mSecondWordFirstInputIndex;
         memcpy(mPrevWord, prevWord->mPrevWord, prevWord->mPrevWordLength * sizeof(mPrevWord[0]));
-        memcpy(mPrevSpacePositions, prevWord->mPrevSpacePositions, sizeof(mPrevSpacePositions));
     }
 
     void init(const int16_t prevWordCount, const int16_t prevWordProbability,
             const int prevWordNodePos, const int *const src0, const int16_t length0,
-            const int *const src1, const int16_t length1, const int *const prevSpacePositions,
-            const int lastInputIndex) {
+            const int *const src1, const int16_t length1,
+            const int prevWordSecondWordFirstInputIndex, const int lastInputIndex) {
         mPrevWordCount = min(prevWordCount, static_cast<int16_t>(MAX_RESULTS));
         mPrevWordProbability = prevWordProbability;
         mPrevWordNodePos = prevWordNodePos;
@@ -80,8 +80,7 @@
         mPrevWord[twoWordsLen] = KEYCODE_SPACE;
         mPrevWordStart = length0;
         mPrevWordLength = static_cast<int16_t>(twoWordsLen + 1);
-        memcpy(mPrevSpacePositions, prevSpacePositions, sizeof(mPrevSpacePositions));
-        mPrevSpacePositions[mPrevWordCount - 1] = lastInputIndex;
+        mSecondWordFirstInputIndex = prevWordSecondWordFirstInputIndex;
     }
 
     void truncate(const int offset) {
@@ -96,11 +95,12 @@
         mPrevWordLength = newPrevWordLength;
     }
 
-    void outputSpacePositions(int *spaceIndices) const {
-        // Convert uint16_t to int
-        for (int i = 0; i < MAX_RESULTS; i++) {
-            spaceIndices[i] = mPrevSpacePositions[i];
-        }
+    void setSecondWordFirstInputIndex(const int inputIndex) {
+        mSecondWordFirstInputIndex = inputIndex;
+    }
+
+    int getSecondWordFirstInputIndex() const {
+        return mSecondWordFirstInputIndex;
     }
 
     // TODO: remove
@@ -138,8 +138,6 @@
 
     // TODO: Move to private
     int mPrevWord[MAX_WORD_LENGTH];
-    // TODO: Move to private
-    int mPrevSpacePositions[MAX_RESULTS];
 
  private:
     // Caution!!!
@@ -150,6 +148,7 @@
     int16_t mPrevWordStart;
     int16_t mPrevWordProbability;
     int mPrevWordNodePos;
+    int mSecondWordFirstInputIndex;
 };
 } // namespace latinime
 #endif // LATINIME_DIC_NODE_STATE_PREVWORD_H
diff --git a/native/jni/src/suggest/core/dictionary/bigram_dictionary.cpp b/native/jni/src/suggest/core/dictionary/bigram_dictionary.cpp
index cf1cd88..425b076 100644
--- a/native/jni/src/suggest/core/dictionary/bigram_dictionary.cpp
+++ b/native/jni/src/suggest/core/dictionary/bigram_dictionary.cpp
@@ -116,7 +116,7 @@
             mDictionaryStructurePolicy->getBigramsStructurePolicy(), pos);
     while (bigramsIt.hasNext()) {
         bigramsIt.next();
-        if (bigramsIt.getBigramPos() == NOT_A_VALID_WORD_POS) {
+        if (bigramsIt.getBigramPos() == NOT_A_DICT_POS) {
             continue;
         }
         const int codePointCount = mDictionaryStructurePolicy->
@@ -146,7 +146,7 @@
     if (0 >= prevWordLength) return NOT_A_DICT_POS;
     int pos = mDictionaryStructurePolicy->getTerminalNodePositionOfWord(prevWord, prevWordLength,
             forceLowerCaseSearch);
-    if (NOT_A_VALID_WORD_POS == pos) return NOT_A_DICT_POS;
+    if (NOT_A_DICT_POS == pos) return NOT_A_DICT_POS;
     return mDictionaryStructurePolicy->getBigramsPositionOfNode(pos);
 }
 
@@ -157,7 +157,7 @@
     if (NOT_A_DICT_POS == pos) return false;
     int nextWordPos = mDictionaryStructurePolicy->getTerminalNodePositionOfWord(word1, length1,
             false /* forceLowerCaseSearch */);
-    if (NOT_A_VALID_WORD_POS == nextWordPos) return false;
+    if (NOT_A_DICT_POS == nextWordPos) return false;
 
     BinaryDictionaryBigramsIterator bigramsIt(
             mDictionaryStructurePolicy->getBigramsStructurePolicy(), pos);
diff --git a/native/jni/src/suggest/core/dictionary/dictionary.cpp b/native/jni/src/suggest/core/dictionary/dictionary.cpp
index 02ece63..0335722 100644
--- a/native/jni/src/suggest/core/dictionary/dictionary.cpp
+++ b/native/jni/src/suggest/core/dictionary/dictionary.cpp
@@ -87,7 +87,7 @@
 int Dictionary::getProbability(const int *word, int length) const {
     int pos = getDictionaryStructurePolicy()->getTerminalNodePositionOfWord(word, length,
             false /* forceLowerCaseSearch */);
-    if (NOT_A_VALID_WORD_POS == pos) {
+    if (NOT_A_DICT_POS == pos) {
         return NOT_A_PROBABILITY;
     }
     return getDictionaryStructurePolicy()->getUnigramProbabilityOfPtNode(pos);
@@ -112,6 +112,18 @@
     mDictionaryStructureWithBufferPolicy->removeBigramWords(word0, length0, word1, length1);
 }
 
+void Dictionary::flush(const char *const filePath) {
+    mDictionaryStructureWithBufferPolicy->flush(filePath);
+}
+
+void Dictionary::flushWithGC(const char *const filePath) {
+    mDictionaryStructureWithBufferPolicy->flushWithGC(filePath);
+}
+
+bool Dictionary::needsToRunGC() {
+    return mDictionaryStructureWithBufferPolicy->needsToRunGC();
+}
+
 void Dictionary::logDictionaryInfo(JNIEnv *const env) const {
     const int BUFFER_SIZE = 16;
     int dictionaryIdCodePointBuffer[BUFFER_SIZE];
diff --git a/native/jni/src/suggest/core/dictionary/dictionary.h b/native/jni/src/suggest/core/dictionary/dictionary.h
index 0afe5a5..06e84bb 100644
--- a/native/jni/src/suggest/core/dictionary/dictionary.h
+++ b/native/jni/src/suggest/core/dictionary/dictionary.h
@@ -77,6 +77,12 @@
     void removeBigramWords(const int *const word0, const int length0, const int *const word1,
             const int length1);
 
+    void flush(const char *const filePath);
+
+    void flushWithGC(const char *const filePath);
+
+    bool needsToRunGC();
+
     const DictionaryStructureWithBufferPolicy *getDictionaryStructurePolicy() const {
         return mDictionaryStructureWithBufferPolicy;
     }
diff --git a/native/jni/src/suggest/core/dictionary/multi_bigram_map.h b/native/jni/src/suggest/core/dictionary/multi_bigram_map.h
index 9efe5f6..aecf413 100644
--- a/native/jni/src/suggest/core/dictionary/multi_bigram_map.h
+++ b/native/jni/src/suggest/core/dictionary/multi_bigram_map.h
@@ -73,7 +73,7 @@
                     bigramsListPos);
             while (bigramsIt.hasNext()) {
                 bigramsIt.next();
-                if (bigramsIt.getBigramPos() == NOT_A_VALID_WORD_POS) {
+                if (bigramsIt.getBigramPos() == NOT_A_DICT_POS) {
                     continue;
                 }
                 mBigramMap[bigramsIt.getBigramPos()] = bigramsIt.getProbability();
diff --git a/native/jni/src/suggest/core/layout/proximity_info_state.cpp b/native/jni/src/suggest/core/layout/proximity_info_state.cpp
index 7780efd..fbabd92 100644
--- a/native/jni/src/suggest/core/layout/proximity_info_state.cpp
+++ b/native/jni/src/suggest/core/layout/proximity_info_state.cpp
@@ -36,8 +36,8 @@
         const int *const xCoordinates, const int *const yCoordinates, const int *const times,
         const int *const pointerIds, const bool isGeometric) {
     ASSERT(isGeometric || (inputSize < MAX_WORD_LENGTH));
-    mIsContinuousSuggestionPossible =
-            ProximityInfoStateUtils::checkAndReturnIsContinuousSuggestionPossible(
+    mIsContinuousSuggestionPossible = (mHasBeenUpdatedByGeometricInput != isGeometric) ?
+            false : ProximityInfoStateUtils::checkAndReturnIsContinuousSuggestionPossible(
                     inputSize, xCoordinates, yCoordinates, times, mSampledInputSize,
                     &mSampledInputXs, &mSampledInputYs, &mSampledTimes, &mSampledInputIndice);
     if (DEBUG_DICT) {
@@ -155,6 +155,7 @@
     if (DEBUG_GEO_FULL) {
         AKLOGI("ProximityState init finished: %d points out of %d", mSampledInputSize, inputSize);
     }
+    mHasBeenUpdatedByGeometricInput = isGeometric;
 }
 
 // This function basically converts from a length to an edit distance. Accordingly, it's obviously
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 dbcd544..c94060f 100644
--- a/native/jni/src/suggest/core/layout/proximity_info_state.h
+++ b/native/jni/src/suggest/core/layout/proximity_info_state.h
@@ -46,10 +46,11 @@
             : mProximityInfo(0), mMaxPointToKeyLength(0.0f), mAverageSpeed(0.0f),
               mHasTouchPositionCorrectionData(false), mMostCommonKeyWidthSquare(0),
               mKeyCount(0), mCellHeight(0), mCellWidth(0), mGridHeight(0), mGridWidth(0),
-              mIsContinuousSuggestionPossible(false), mSampledInputXs(), mSampledInputYs(),
-              mSampledTimes(), mSampledInputIndice(), mSampledLengthCache(),
-              mBeelineSpeedPercentiles(), mSampledNormalizedSquaredLengthCache(), mSpeedRates(),
-              mDirections(), mCharProbabilities(), mSampledNearKeySets(), mSampledSearchKeySets(),
+              mIsContinuousSuggestionPossible(false), mHasBeenUpdatedByGeometricInput(false),
+              mSampledInputXs(), mSampledInputYs(), mSampledTimes(), mSampledInputIndice(),
+              mSampledLengthCache(), mBeelineSpeedPercentiles(),
+              mSampledNormalizedSquaredLengthCache(), mSpeedRates(), mDirections(),
+              mCharProbabilities(), mSampledNearKeySets(), mSampledSearchKeySets(),
               mSampledSearchKeyVectors(), mTouchPositionCorrectionEnabled(false),
               mSampledInputSize(0), mMostProbableStringProbability(0.0f) {
         memset(mInputProximities, 0, sizeof(mInputProximities));
@@ -129,6 +130,10 @@
         return mSampledInputYs[index];
     }
 
+    int getInputIndexOfSampledPoint(const int sampledIndex) const {
+        return mSampledInputIndice[sampledIndex];
+    }
+
     bool hasSpaceProximity(const int index) const;
 
     int getLengthCache(const int index) const {
@@ -204,6 +209,7 @@
     int mGridHeight;
     int mGridWidth;
     bool mIsContinuousSuggestionPossible;
+    bool mHasBeenUpdatedByGeometricInput;
 
     std::vector<int> mSampledInputXs;
     std::vector<int> mSampledInputYs;
diff --git a/native/jni/src/suggest/core/policy/dictionary_structure_with_buffer_policy.h b/native/jni/src/suggest/core/policy/dictionary_structure_with_buffer_policy.h
index c8cbbcf..24d1b8b 100644
--- a/native/jni/src/suggest/core/policy/dictionary_structure_with_buffer_policy.h
+++ b/native/jni/src/suggest/core/policy/dictionary_structure_with_buffer_policy.h
@@ -74,6 +74,12 @@
     virtual bool removeBigramWords(const int *const word0, const int length0,
             const int *const word1, const int length1) = 0;
 
+    virtual void flush(const char *const filePath) = 0;
+
+    virtual void flushWithGC(const char *const filePath) = 0;
+
+    virtual bool needsToRunGC() const = 0;
+
  protected:
     DictionaryStructureWithBufferPolicy() {}
 
diff --git a/native/jni/src/suggest/core/session/dic_traverse_session.cpp b/native/jni/src/suggest/core/session/dic_traverse_session.cpp
index 0ca583f..50f2bbd 100644
--- a/native/jni/src/suggest/core/session/dic_traverse_session.cpp
+++ b/native/jni/src/suggest/core/session/dic_traverse_session.cpp
@@ -23,6 +23,11 @@
 
 namespace latinime {
 
+// 256K bytes threshold is heuristically used to distinguish dictionaries containing many unigrams
+// (e.g. main dictionary) from small dictionaries (e.g. contacts...)
+const int DicTraverseSession::DICTIONARY_SIZE_THRESHOLD_TO_USE_LARGE_CACHE_FOR_SUGGESTION =
+        256 * 1024;
+
 void DicTraverseSession::init(const Dictionary *const dictionary, const int *prevWord,
         int prevWordLength, const SuggestOptions *const suggestOptions) {
     mDictionary = dictionary;
@@ -30,13 +35,13 @@
             ->getMultiWordCostMultiplier();
     mSuggestOptions = suggestOptions;
     if (!prevWord) {
-        mPrevWordPos = NOT_A_VALID_WORD_POS;
+        mPrevWordPos = NOT_A_DICT_POS;
         return;
     }
     // TODO: merge following similar calls to getTerminalPosition into one case-insensitive call.
     mPrevWordPos = getDictionaryStructurePolicy()->getTerminalNodePositionOfWord(
             prevWord, prevWordLength, false /* forceLowerCaseSearch */);
-    if (mPrevWordPos == NOT_A_VALID_WORD_POS) {
+    if (mPrevWordPos == NOT_A_DICT_POS) {
         // Check bigrams for lower-cased previous word if original was not found. Useful for
         // auto-capitalized words like "The [current_word]".
         mPrevWordPos = getDictionaryStructurePolicy()->getTerminalNodePositionOfWord(
@@ -59,8 +64,9 @@
     return mDictionary->getDictionaryStructurePolicy();
 }
 
-void DicTraverseSession::resetCache(const int nextActiveCacheSize, const int maxWords) {
-    mDicNodesCache.reset(nextActiveCacheSize, maxWords);
+void DicTraverseSession::resetCache(const int thresholdForNextActiveDicNodes, const int maxWords) {
+    mDicNodesCache.reset(thresholdForNextActiveDicNodes /* nextActiveSize */,
+            maxWords /* terminalSize */);
     mMultiBigramMap.clear();
     mPartiallyCommited = false;
 }
diff --git a/native/jni/src/suggest/core/session/dic_traverse_session.h b/native/jni/src/suggest/core/session/dic_traverse_session.h
index 23de5cc..e0b1c67 100644
--- a/native/jni/src/suggest/core/session/dic_traverse_session.h
+++ b/native/jni/src/suggest/core/session/dic_traverse_session.h
@@ -37,8 +37,12 @@
  public:
 
     // A factory method for DicTraverseSession
-    static AK_FORCE_INLINE void *getSessionInstance(JNIEnv *env, jstring localeStr) {
-        return new DicTraverseSession(env, localeStr);
+    static AK_FORCE_INLINE void *getSessionInstance(JNIEnv *env, jstring localeStr,
+            jlong dictSize) {
+        // To deal with the trade-off between accuracy and memory space, large cache is used for
+        // dictionaries larger that the threshold
+        return new DicTraverseSession(env, localeStr,
+                dictSize >= DICTIONARY_SIZE_THRESHOLD_TO_USE_LARGE_CACHE_FOR_SUGGESTION);
     }
 
     static AK_FORCE_INLINE void initSessionInstance(DicTraverseSession *traverseSession,
@@ -54,10 +58,10 @@
         delete traverseSession;
     }
 
-    AK_FORCE_INLINE DicTraverseSession(JNIEnv *env, jstring localeStr)
-            : mPrevWordPos(NOT_A_VALID_WORD_POS), mProximityInfo(0),
-              mDictionary(0), mSuggestOptions(0), mDicNodesCache(), mMultiBigramMap(),
-              mInputSize(0), mPartiallyCommited(false), mMaxPointerCount(1),
+    AK_FORCE_INLINE DicTraverseSession(JNIEnv *env, jstring localeStr, bool usesLargeCache)
+            : mPrevWordPos(NOT_A_DICT_POS), mProximityInfo(0),
+              mDictionary(0), mSuggestOptions(0), mDicNodesCache(usesLargeCache),
+              mMultiBigramMap(), mInputSize(0), mPartiallyCommited(false), mMaxPointerCount(1),
               mMultiWordCostMultiplier(1.0f) {
         // NOTE: mProximityInfoStates is an array of instances.
         // No need to initialize it explicitly here.
@@ -73,7 +77,7 @@
             const int inputSize, const int *const inputXs, const int *const inputYs,
             const int *const times, const int *const pointerIds, const float maxSpatialDistance,
             const int maxPointerCount);
-    void resetCache(const int nextActiveCacheSize, const int maxWords);
+    void resetCache(const int thresholdForNextActiveDicNodes, const int maxWords);
 
     const DictionaryStructureWithBufferPolicy *getDictionaryStructurePolicy() const;
 
@@ -109,7 +113,9 @@
         if (usedPointerCount != 1) {
             return false;
         }
-        *pointerId = usedPointerId;
+        if (pointerId) {
+            *pointerId = usedPointerId;
+        }
         return true;
     }
 
@@ -181,6 +187,7 @@
     DISALLOW_IMPLICIT_CONSTRUCTORS(DicTraverseSession);
     // threshold to start caching
     static const int CACHE_START_INPUT_LENGTH_THRESHOLD;
+    static const int DICTIONARY_SIZE_THRESHOLD_TO_USE_LARGE_CACHE_FOR_SUGGESTION;
     void initializeProximityInfoStates(const int *const inputCodePoints, const int *const inputXs,
             const int *const inputYs, const int *const times, const int *const pointerIds,
             const int inputSize, const float maxSpatialDistance, const int maxPointerCount);
diff --git a/native/jni/src/suggest/core/suggest.cpp b/native/jni/src/suggest/core/suggest.cpp
index e788e91..0c925be 100644
--- a/native/jni/src/suggest/core/suggest.cpp
+++ b/native/jni/src/suggest/core/suggest.cpp
@@ -117,7 +117,7 @@
  * Outputs the final list of suggestions (i.e., terminal nodes).
  */
 int Suggest::outputSuggestions(DicTraverseSession *traverseSession, int *frequencies,
-        int *outputCodePoints, int *spaceIndices, int *outputTypes) const {
+        int *outputCodePoints, int *outputIndicesToPartialCommit, int *outputTypes) const {
 #if DEBUG_EVALUATE_MOST_PROBABLE_STRING
     const int terminalSize = 0;
 #else
@@ -139,6 +139,7 @@
             SCORING->getMostProbableString(traverseSession, terminalSize, languageWeight,
                     &outputCodePoints[0], &outputTypes[0], &frequencies[0]);
     if (hasMostProbableString) {
+        outputIndicesToPartialCommit[outputWordIndex] = NOT_AN_INDEX;
         ++outputWordIndex;
     }
 
@@ -160,6 +161,9 @@
                             || (traverseSession->getInputSize()
                                     >= MIN_LEN_FOR_MULTI_WORD_AUTOCORRECT
                                             && terminals[0].hasMultipleWords())) : false;
+    // TODO: have partial commit work even with multiple pointers.
+    const bool outputSecondWordFirstLetterInputIndex =
+            traverseSession->isOnlyOnePointerUsed(0 /* pointerId */);
     // Output suggestion results here
     for (int terminalIndex = 0; terminalIndex < terminalSize && outputWordIndex < MAX_RESULTS;
             ++terminalIndex) {
@@ -194,18 +198,21 @@
                 terminalDicNode->isExactMatch()
                         || (forceCommitMultiWords && terminalDicNode->hasMultipleWords())
                                 || (isValidWord && SCORING->doesAutoCorrectValidWord()));
-        maxScore = max(maxScore, finalScore);
-
-        // TODO: Implement a smarter auto-commit method for handling multi-word suggestions.
-        // Index for top typing suggestion should be 0.
-        if (isValidWord && outputWordIndex == 0) {
-            terminalDicNode->outputSpacePositionsResult(spaceIndices);
+        if (maxScore < finalScore && isValidWord) {
+            maxScore = finalScore;
         }
 
         // Don't output invalid words. However, we still need to submit their shortcuts if any.
         if (isValidWord) {
             outputTypes[outputWordIndex] = Dictionary::KIND_CORRECTION | outputTypeFlags;
             frequencies[outputWordIndex] = finalScore;
+            if (outputSecondWordFirstLetterInputIndex) {
+                outputIndicesToPartialCommit[outputWordIndex] =
+                        terminalDicNode->getSecondWordFirstInputIndex(
+                                traverseSession->getProximityInfoState(0));
+            } else {
+                outputIndicesToPartialCommit[outputWordIndex] = NOT_AN_INDEX;
+            }
             // Populate the outputChars array with the suggested word.
             const int startIndex = outputWordIndex * MAX_WORD_LENGTH;
             terminalDicNode->outputResult(&outputCodePoints[startIndex]);
@@ -220,8 +227,19 @@
             // Shortcut is not supported for multiple words suggestions.
             // TODO: Check shortcuts during traversal for multiple words suggestions.
             const bool sameAsTyped = TRAVERSAL->sameAsTyped(traverseSession, terminalDicNode);
-            outputWordIndex = ShortcutUtils::outputShortcuts(&shortcutIt, outputWordIndex,
-                    finalScore, outputCodePoints, frequencies, outputTypes, sameAsTyped);
+            const int updatedOutputWordIndex = ShortcutUtils::outputShortcuts(&shortcutIt,
+                    outputWordIndex,  finalScore, outputCodePoints, frequencies, outputTypes,
+                    sameAsTyped);
+            const int secondWordFirstInputIndex = terminalDicNode->getSecondWordFirstInputIndex(
+                    traverseSession->getProximityInfoState(0));
+            for (int i = outputWordIndex; i < updatedOutputWordIndex; ++i) {
+                if (outputSecondWordFirstLetterInputIndex) {
+                    outputIndicesToPartialCommit[i] = secondWordFirstInputIndex;
+                } else {
+                    outputIndicesToPartialCommit[i] = NOT_AN_INDEX;
+                }
+            }
+            outputWordIndex = updatedOutputWordIndex;
         }
         DicNode::managedDelete(terminalDicNode);
     }
diff --git a/native/jni/src/suggest/core/suggest.h b/native/jni/src/suggest/core/suggest.h
index 875cbe4..b240196 100644
--- a/native/jni/src/suggest/core/suggest.h
+++ b/native/jni/src/suggest/core/suggest.h
@@ -55,7 +55,7 @@
     void createNextWordDicNode(DicTraverseSession *traverseSession, DicNode *dicNode,
             const bool spaceSubstitution) const;
     int outputSuggestions(DicTraverseSession *traverseSession, int *frequencies,
-            int *outputCodePoints, int *outputIndices, int *outputTypes) const;
+            int *outputCodePoints, int *outputIndicesToPartialCommit, int *outputTypes) const;
     void initializeSearch(DicTraverseSession *traverseSession, int commitPoint) const;
     void expandCurrentDicNodes(DicTraverseSession *traverseSession) const;
     void processTerminalDicNode(DicTraverseSession *traverseSession, DicNode *dicNode) const;
diff --git a/native/jni/src/suggest/policyimpl/dictionary/bigram/bigram_list_read_write_utils.cpp b/native/jni/src/suggest/policyimpl/dictionary/bigram/bigram_list_read_write_utils.cpp
index d575474..09e832f 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/bigram/bigram_list_read_write_utils.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/bigram/bigram_list_read_write_utils.cpp
@@ -38,6 +38,22 @@
         BigramListReadWriteUtils::MASK_ATTRIBUTE_PROBABILITY = 0x0F;
 const int BigramListReadWriteUtils::ATTRIBUTE_ADDRESS_SHIFT = 4;
 
+/* static */ BigramListReadWriteUtils::BigramFlags
+        BigramListReadWriteUtils::getFlagsAndForwardPointer(const uint8_t *const bigramsBuf,
+                int *const pos) {
+    return ByteArrayUtils::readUint8AndAdvancePosition(bigramsBuf, pos);
+}
+
+/* static */ void BigramListReadWriteUtils::skipExistingBigrams(const uint8_t *const bigramsBuf,
+        int *const pos) {
+    BigramFlags flags = getFlagsAndForwardPointer(bigramsBuf, pos);
+    while (hasNext(flags)) {
+        *pos += attributeAddressSize(flags);
+        flags = getFlagsAndForwardPointer(bigramsBuf, pos);
+    }
+    *pos += attributeAddressSize(flags);
+}
+
 /* static */ int BigramListReadWriteUtils::getBigramAddressAndForwardPointer(
         const uint8_t *const bigramsBuf, const BigramFlags flags, int *const pos) {
     int offset = 0;
@@ -54,7 +70,7 @@
             break;
     }
     if (offset == 0) {
-        return NOT_A_VALID_WORD_POS;
+        return NOT_A_DICT_POS;
     }
     if (isOffsetNegative(flags)) {
         return origin - offset;
diff --git a/native/jni/src/suggest/policyimpl/dictionary/bigram/bigram_list_read_write_utils.h b/native/jni/src/suggest/policyimpl/dictionary/bigram/bigram_list_read_write_utils.h
index ee2b722..884bcd7 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/bigram/bigram_list_read_write_utils.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/bigram/bigram_list_read_write_utils.h
@@ -21,7 +21,6 @@
 #include <stdint.h>
 
 #include "defines.h"
-#include "suggest/policyimpl/dictionary/utils/byte_array_utils.h"
 
 namespace latinime {
 
@@ -29,10 +28,7 @@
 public:
    typedef uint8_t BigramFlags;
 
-   static AK_FORCE_INLINE BigramFlags getFlagsAndForwardPointer(
-           const uint8_t *const bigramsBuf, int *const pos) {
-       return ByteArrayUtils::readUint8AndAdvancePosition(bigramsBuf, pos);
-   }
+   static BigramFlags getFlagsAndForwardPointer(const uint8_t *const bigramsBuf, int *const pos);
 
    static AK_FORCE_INLINE int getProbabilityFromFlags(const BigramFlags flags) {
        return flags & MASK_ATTRIBUTE_PROBABILITY;
@@ -43,15 +39,7 @@
    }
 
    // Bigrams reading methods
-   static AK_FORCE_INLINE void skipExistingBigrams(const uint8_t *const bigramsBuf,
-           int *const pos) {
-       BigramFlags flags = getFlagsAndForwardPointer(bigramsBuf, pos);
-       while (hasNext(flags)) {
-           *pos += attributeAddressSize(flags);
-           flags = getFlagsAndForwardPointer(bigramsBuf, pos);
-       }
-       *pos += attributeAddressSize(flags);
-   }
+   static void skipExistingBigrams(const uint8_t *const bigramsBuf, int *const pos);
 
    static int getBigramAddressAndForwardPointer(const uint8_t *const bigramsBuf,
            const BigramFlags flags, int *const pos);
@@ -74,12 +62,17 @@
        return flags | FLAG_ATTRIBUTE_HAS_NEXT;
    }
 
+   static AK_FORCE_INLINE BigramFlags setProbabilityInFlags(const BigramFlags flags,
+           const int probability) {
+       return (flags & (~MASK_ATTRIBUTE_PROBABILITY)) | (probability & MASK_ATTRIBUTE_PROBABILITY);
+   }
+
    // Returns true if the bigram entry is valid and put entry values into out*.
    static AK_FORCE_INLINE bool createBigramEntryAndGetFlagsAndOffsetAndOffsetFieldSize(
            const int entryPos, const int targetPos, const int probability, const bool hasNext,
            BigramFlags *const outBigramFlags, uint32_t *const outOffset,
            int *const outOffsetFieldSize) {
-       if (targetPos == NOT_A_VALID_WORD_POS) {
+       if (targetPos == NOT_A_DICT_POS) {
            return false;
        }
        BigramFlags flags = probability & MASK_ATTRIBUTE_PROBABILITY;
diff --git a/native/jni/src/suggest/policyimpl/dictionary/bigram/dynamic_bigram_list_policy.cpp b/native/jni/src/suggest/policyimpl/dictionary/bigram/dynamic_bigram_list_policy.cpp
index 936dc9c..4c44d22 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/bigram/dynamic_bigram_list_policy.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/bigram/dynamic_bigram_list_policy.cpp
@@ -18,25 +18,64 @@
 
 namespace latinime {
 
-bool DynamicBigramListPolicy::copyAllBigrams(int *const fromPos, int *const toPos) {
+const int DynamicBigramListPolicy::BIGRAM_LINK_COUNT_LIMIT = 10000;
+
+void DynamicBigramListPolicy::getNextBigram(int *const outBigramPos, int *const outProbability,
+        bool *const outHasNext, int *const pos) const {
+    const bool usesAdditionalBuffer = mBuffer->isInAdditionalBuffer(*pos);
+    const uint8_t *const buffer = mBuffer->getBuffer(usesAdditionalBuffer);
+    if (usesAdditionalBuffer) {
+        *pos -= mBuffer->getOriginalBufferSize();
+    }
+    const BigramListReadWriteUtils::BigramFlags flags =
+            BigramListReadWriteUtils::getFlagsAndForwardPointer(buffer, pos);
+    int originalBigramPos = BigramListReadWriteUtils::getBigramAddressAndForwardPointer(
+            buffer, flags, pos);
+    if (usesAdditionalBuffer && originalBigramPos != NOT_A_DICT_POS) {
+        originalBigramPos += mBuffer->getOriginalBufferSize();
+    }
+    *outBigramPos = followBigramLinkAndGetCurrentBigramPtNodePos(originalBigramPos);
+    *outProbability = BigramListReadWriteUtils::getProbabilityFromFlags(flags);
+    *outHasNext = BigramListReadWriteUtils::hasNext(flags);
+    if (usesAdditionalBuffer) {
+        *pos += mBuffer->getOriginalBufferSize();
+    }
+}
+
+void DynamicBigramListPolicy::skipAllBigrams(int *const pos) const {
+    const bool usesAdditionalBuffer = mBuffer->isInAdditionalBuffer(*pos);
+    const uint8_t *const buffer = mBuffer->getBuffer(usesAdditionalBuffer);
+    if (usesAdditionalBuffer) {
+        *pos -= mBuffer->getOriginalBufferSize();
+    }
+    BigramListReadWriteUtils::skipExistingBigrams(buffer, pos);
+    if (usesAdditionalBuffer) {
+        *pos += mBuffer->getOriginalBufferSize();
+    }
+}
+
+bool DynamicBigramListPolicy::copyAllBigrams(int *const fromPos, int *const toPos,
+        int *outBigramsCount) {
     const bool usesAdditionalBuffer = mBuffer->isInAdditionalBuffer(*fromPos);
     if (usesAdditionalBuffer) {
         *fromPos -= mBuffer->getOriginalBufferSize();
     }
+    *outBigramsCount = 0;
     BigramListReadWriteUtils::BigramFlags flags;
     do {
         // The buffer address can be changed after calling buffer writing methods.
         const uint8_t *const buffer = mBuffer->getBuffer(usesAdditionalBuffer);
         flags = BigramListReadWriteUtils::getFlagsAndForwardPointer(buffer, fromPos);
-        int bigramPos = BigramListReadWriteUtils::getBigramAddressAndForwardPointer(
+        int originalBigramPos = BigramListReadWriteUtils::getBigramAddressAndForwardPointer(
                 buffer, flags, fromPos);
-        if (bigramPos == NOT_A_VALID_WORD_POS) {
+        if (originalBigramPos == NOT_A_DICT_POS) {
             // skip invalid bigram entry.
             continue;
         }
         if (usesAdditionalBuffer) {
-            bigramPos += mBuffer->getOriginalBufferSize();
+            originalBigramPos += mBuffer->getOriginalBufferSize();
         }
+        const int bigramPos = followBigramLinkAndGetCurrentBigramPtNodePos(originalBigramPos);
         BigramListReadWriteUtils::BigramFlags newBigramFlags;
         uint32_t newBigramOffset;
         int newBigramOffsetFieldSize;
@@ -54,6 +93,7 @@
                 toPos)) {
             return false;
         }
+        (*outBigramsCount)++;
     } while(BigramListReadWriteUtils::hasNext(flags));
     if (usesAdditionalBuffer) {
         *fromPos += mBuffer->getOriginalBufferSize();
@@ -61,8 +101,8 @@
     return true;
 }
 
-bool DynamicBigramListPolicy::addBigramEntry(const int bigramPos, const int probability,
-        int *const pos) {
+bool DynamicBigramListPolicy::addNewBigramEntryToBigramList(const int bigramPos,
+        const int probability, int *const pos) {
     const bool usesAdditionalBuffer = mBuffer->isInAdditionalBuffer(*pos);
     if (usesAdditionalBuffer) {
         *pos -= mBuffer->getOriginalBufferSize();
@@ -76,7 +116,17 @@
         // The buffer address can be changed after calling buffer writing methods.
         const uint8_t *const buffer = mBuffer->getBuffer(usesAdditionalBuffer);
         flags = BigramListReadWriteUtils::getFlagsAndForwardPointer(buffer, pos);
-        BigramListReadWriteUtils::getBigramAddressAndForwardPointer(buffer, flags, pos);
+        int originalBigramPos = BigramListReadWriteUtils::getBigramAddressAndForwardPointer(
+                buffer, flags, pos);
+        if (usesAdditionalBuffer && originalBigramPos != NOT_A_DICT_POS) {
+            originalBigramPos += mBuffer->getOriginalBufferSize();
+        }
+        if (followBigramLinkAndGetCurrentBigramPtNodePos(originalBigramPos) == bigramPos) {
+            // Update this bigram entry.
+            const BigramListReadWriteUtils::BigramFlags updatedFlags =
+                    BigramListReadWriteUtils::setProbabilityInFlags(flags, probability);
+            return mBuffer->writeUintAndAdvancePosition(updatedFlags, 1 /* size */, &entryPos);
+        }
         if (BigramListReadWriteUtils::hasNext(flags)) {
             continue;
         }
@@ -87,33 +137,35 @@
         if (!mBuffer->writeUintAndAdvancePosition(updatedFlags, 1 /* size */, &entryPos)) {
             return false;
         }
-        // Then, add a new entry after the last entry.
-        BigramListReadWriteUtils::BigramFlags newBigramFlags;
-        uint32_t newBigramOffset;
-        int newBigramOffsetFieldSize;
-        if(!BigramListReadWriteUtils::createBigramEntryAndGetFlagsAndOffsetAndOffsetFieldSize(
-                *pos, bigramPos, BigramListReadWriteUtils::getProbabilityFromFlags(flags),
-                BigramListReadWriteUtils::hasNext(flags), &newBigramFlags, &newBigramOffset,
-                &newBigramOffsetFieldSize)) {
-            continue;
-        }
-        int newEntryPos = *pos;
         if (usesAdditionalBuffer) {
-            newEntryPos += mBuffer->getOriginalBufferSize();
+            *pos += mBuffer->getOriginalBufferSize();
         }
-        // Write bigram flags.
-        if (!mBuffer->writeUintAndAdvancePosition(newBigramFlags, 1 /* size */,
-                &newEntryPos)) {
-            return false;
-        }
-        // Write bigram positon offset.
-        if (!mBuffer->writeUintAndAdvancePosition(newBigramOffset, newBigramOffsetFieldSize,
-                &newEntryPos)) {
-            return false;
-        }
+        // Then, add a new entry after the last entry.
+        return writeNewBigramEntry(bigramPos, probability, pos);
     } while(BigramListReadWriteUtils::hasNext(flags));
-    if (usesAdditionalBuffer) {
-        *pos += mBuffer->getOriginalBufferSize();
+    // We return directly from the while loop.
+    ASSERT(false);
+    return false;
+}
+
+bool DynamicBigramListPolicy::writeNewBigramEntry(const int bigramPos, const int probability,
+        int *const writingPos) {
+    BigramListReadWriteUtils::BigramFlags newBigramFlags;
+    uint32_t newBigramOffset;
+    int newBigramOffsetFieldSize;
+    if(!BigramListReadWriteUtils::createBigramEntryAndGetFlagsAndOffsetAndOffsetFieldSize(
+            *writingPos, bigramPos, probability, false /* hasNext */, &newBigramFlags,
+            &newBigramOffset, &newBigramOffsetFieldSize)) {
+        return false;
+    }
+    // Write bigram flags.
+    if (!mBuffer->writeUintAndAdvancePosition(newBigramFlags, 1 /* size */, writingPos)) {
+        return false;
+    }
+    // Write bigram positon offset.
+    if (!mBuffer->writeUintAndAdvancePosition(newBigramOffset, newBigramOffsetFieldSize,
+            writingPos)) {
+        return false;
     }
     return true;
 }
@@ -133,11 +185,12 @@
         if (usesAdditionalBuffer) {
             bigramOffsetFieldPos += mBuffer->getOriginalBufferSize();
         }
-        int bigramPos = BigramListReadWriteUtils::getBigramAddressAndForwardPointer(
+        int originalBigramPos = BigramListReadWriteUtils::getBigramAddressAndForwardPointer(
                 buffer, flags, &pos);
-        if (usesAdditionalBuffer && bigramPos != NOT_A_VALID_WORD_POS) {
-            bigramPos += mBuffer->getOriginalBufferSize();
+        if (usesAdditionalBuffer && originalBigramPos != NOT_A_DICT_POS) {
+            originalBigramPos += mBuffer->getOriginalBufferSize();
         }
+        const int bigramPos = followBigramLinkAndGetCurrentBigramPtNodePos(originalBigramPos);
         if (bigramPos != targetBigramPos) {
             continue;
         }
@@ -152,4 +205,26 @@
     return false;
 }
 
+int DynamicBigramListPolicy::followBigramLinkAndGetCurrentBigramPtNodePos(
+        const int originalBigramPos) const {
+    if (originalBigramPos == NOT_A_DICT_POS) {
+        return NOT_A_DICT_POS;
+    }
+    int currentPos = originalBigramPos;
+    DynamicPatriciaTrieNodeReader nodeReader(mBuffer, this /* bigramsPolicy */, mShortcutPolicy);
+    nodeReader.fetchNodeInfoFromBuffer(currentPos);
+    int bigramLinkCount = 0;
+    while (nodeReader.getBigramLinkedNodePos() != NOT_A_DICT_POS) {
+        currentPos = nodeReader.getBigramLinkedNodePos();
+        nodeReader.fetchNodeInfoFromBuffer(currentPos);
+        bigramLinkCount++;
+        if (bigramLinkCount > BIGRAM_LINK_COUNT_LIMIT) {
+            AKLOGI("Bigram link is invalid. start position: %d", bigramPos);
+            ASSERT(false);
+            return NOT_A_DICT_POS;
+        }
+    }
+    return currentPos;
+}
+
 } // namespace latinime
diff --git a/native/jni/src/suggest/policyimpl/dictionary/bigram/dynamic_bigram_list_policy.h b/native/jni/src/suggest/policyimpl/dictionary/bigram/dynamic_bigram_list_policy.h
index b7c0537..dafb62d 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/bigram/dynamic_bigram_list_policy.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/bigram/dynamic_bigram_list_policy.h
@@ -21,7 +21,9 @@
 
 #include "defines.h"
 #include "suggest/core/policy/dictionary_bigrams_structure_policy.h"
+#include "suggest/core/policy/dictionary_shortcuts_structure_policy.h"
 #include "suggest/policyimpl/dictionary/bigram/bigram_list_read_write_utils.h"
+#include "suggest/policyimpl/dictionary/dynamic_patricia_trie_reading_helper.h"
 #include "suggest/policyimpl/dictionary/utils/buffer_with_extendable_buffer.h"
 
 namespace latinime {
@@ -31,49 +33,26 @@
  */
 class DynamicBigramListPolicy : public DictionaryBigramsStructurePolicy {
  public:
-    DynamicBigramListPolicy(BufferWithExtendableBuffer *const buffer)
-            : mBuffer(buffer) {}
+    DynamicBigramListPolicy(BufferWithExtendableBuffer *const buffer,
+            const DictionaryShortcutsStructurePolicy *const shortcutPolicy)
+            : mBuffer(buffer), mShortcutPolicy(shortcutPolicy) {}
 
     ~DynamicBigramListPolicy() {}
 
     void getNextBigram(int *const outBigramPos, int *const outProbability, bool *const outHasNext,
-            int *const pos) const {
-        const bool usesAdditionalBuffer = mBuffer->isInAdditionalBuffer(*pos);
-        const uint8_t *const buffer = mBuffer->getBuffer(usesAdditionalBuffer);
-        if (usesAdditionalBuffer) {
-            *pos -= mBuffer->getOriginalBufferSize();
-        }
-        const BigramListReadWriteUtils::BigramFlags flags =
-                BigramListReadWriteUtils::getFlagsAndForwardPointer(buffer, pos);
-        *outBigramPos = BigramListReadWriteUtils::getBigramAddressAndForwardPointer(
-                buffer, flags, pos);
-        if (usesAdditionalBuffer && *outBigramPos != NOT_A_VALID_WORD_POS) {
-            *outBigramPos += mBuffer->getOriginalBufferSize();
-        }
-        *outProbability = BigramListReadWriteUtils::getProbabilityFromFlags(flags);
-        *outHasNext = BigramListReadWriteUtils::hasNext(flags);
-        if (usesAdditionalBuffer) {
-            *pos += mBuffer->getOriginalBufferSize();
-        }
-    }
+            int *const pos) const;
 
-    void skipAllBigrams(int *const pos) const {
-        const bool usesAdditionalBuffer = mBuffer->isInAdditionalBuffer(*pos);
-        const uint8_t *const buffer = mBuffer->getBuffer(usesAdditionalBuffer);
-        if (usesAdditionalBuffer) {
-            *pos -= mBuffer->getOriginalBufferSize();
-        }
-        BigramListReadWriteUtils::skipExistingBigrams(buffer, pos);
-        if (usesAdditionalBuffer) {
-            *pos += mBuffer->getOriginalBufferSize();
-        }
-    }
+    void skipAllBigrams(int *const pos) const;
 
     // Copy bigrams from the bigram list that starts at fromPos to toPos and advance these
-    // positions after bigram lists. This method skips invalid bigram entries.
-    bool copyAllBigrams(int *const fromPos, int *const toPos);
+    // positions after bigram lists. This method skips invalid bigram entries and write the valid
+    // bigram entry count to outBigramsCount.
+    bool copyAllBigrams(int *const fromPos, int *const toPos, int *outBigramsCount);
 
-    bool addBigramEntry(const int bigramPos, const int probability, int *const pos);
+    bool addNewBigramEntryToBigramList(const int bigramPos, const int probability, int *const pos);
+
+    bool writeNewBigramEntry(const int bigramPos, const int probability,
+            int *const writingPos);
 
     // Return if targetBigramPos is found or not.
     bool removeBigram(const int bigramListPos, const int targetBigramPos);
@@ -81,7 +60,13 @@
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(DynamicBigramListPolicy);
 
+    static const int BIGRAM_LINK_COUNT_LIMIT;
+
     BufferWithExtendableBuffer *const mBuffer;
+    const DictionaryShortcutsStructurePolicy *const mShortcutPolicy;
+
+    // Follow bigram link and return the position of bigram target PtNode that is currently valid.
+    int followBigramLinkAndGetCurrentBigramPtNodePos(const int originalBigramPos) const;
 };
 } // namespace latinime
 #endif // LATINIME_DYNAMIC_BIGRAM_LIST_POLICY_H
diff --git a/native/jni/src/suggest/policyimpl/dictionary/dictionary_structure_with_buffer_policy_factory.cpp b/native/jni/src/suggest/policyimpl/dictionary/dictionary_structure_with_buffer_policy_factory.cpp
index 434858d..ff80dd2 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/dictionary_structure_with_buffer_policy_factory.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/dictionary_structure_with_buffer_policy_factory.cpp
@@ -27,12 +27,12 @@
 namespace latinime {
 
 /* static */ DictionaryStructureWithBufferPolicy *DictionaryStructureWithBufferPolicyFactory
-        ::newDictionaryStructureWithBufferPolicy(const char *const path, const int pathLength,
-                const int bufOffset, const int size, const bool isUpdatable) {
+        ::newDictionaryStructureWithBufferPolicy(const char *const path, const int bufOffset,
+                const int size, const bool isUpdatable) {
     // Allocated buffer in MmapedBuffer::openBuffer() will be freed in the destructor of
     // impl classes of DictionaryStructureWithBufferPolicy.
-    const MmappedBuffer *const mmapedBuffer = MmappedBuffer::openBuffer(path, pathLength, bufOffset,
-            size, isUpdatable);
+    const MmappedBuffer *const mmapedBuffer = MmappedBuffer::openBuffer(path, bufOffset, size,
+            isUpdatable);
     if (!mmapedBuffer) {
         return 0;
     }
diff --git a/native/jni/src/suggest/policyimpl/dictionary/dictionary_structure_with_buffer_policy_factory.h b/native/jni/src/suggest/policyimpl/dictionary/dictionary_structure_with_buffer_policy_factory.h
index 1cb7a89..8cebc3b 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/dictionary_structure_with_buffer_policy_factory.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/dictionary_structure_with_buffer_policy_factory.h
@@ -27,8 +27,7 @@
 class DictionaryStructureWithBufferPolicyFactory {
  public:
     static DictionaryStructureWithBufferPolicy *newDictionaryStructureWithBufferPolicy(
-            const char *const path, const int pathLength, const int bufOffset, const int size,
-            const bool isUpdatable);
+            const char *const path, const int bufOffset, const int size, const bool isUpdatable);
 
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(DictionaryStructureWithBufferPolicyFactory);
diff --git a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_node_reader.cpp b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_node_reader.cpp
index 5674cb4..7370984 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_node_reader.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_node_reader.cpp
@@ -25,9 +25,17 @@
 
 void DynamicPatriciaTrieNodeReader::fetchNodeInfoFromBufferAndProcessMovedNode(const int nodePos,
         const int maxCodePointCount, int *const outCodePoints) {
+    if (nodePos < 0 || nodePos >= mBuffer->getTailPosition()) {
+        AKLOGE("Fetching PtNode info form invalid dictionary position: %d, dictionary size: %d",
+                nodePos, mBuffer->getTailPosition());
+        ASSERT(false);
+        invalidatePtNodeInfo();
+        return;
+    }
     const bool usesAdditionalBuffer = mBuffer->isInAdditionalBuffer(nodePos);
     const uint8_t *const dictBuf = mBuffer->getBuffer(usesAdditionalBuffer);
     int pos = nodePos;
+    mHeadPos = nodePos;
     if (usesAdditionalBuffer) {
         pos -= mBuffer->getOriginalBufferSize();
     }
@@ -57,10 +65,17 @@
         mChildrenPosFieldPos += mBuffer->getOriginalBufferSize();
     }
     mChildrenPos = DynamicPatriciaTrieReadingUtils::readChildrenPositionAndAdvancePosition(
-            dictBuf, mFlags, &pos);
+            dictBuf, &pos);
     if (usesAdditionalBuffer && mChildrenPos != NOT_A_DICT_POS) {
         mChildrenPos += mBuffer->getOriginalBufferSize();
     }
+    if (mSiblingPos == NOT_A_DICT_POS) {
+        if (DynamicPatriciaTrieReadingUtils::isMoved(mFlags)) {
+            mBigramLinkedNodePos = mChildrenPos;
+        } else {
+            mBigramLinkedNodePos = NOT_A_DICT_POS;
+        }
+    }
     if (usesAdditionalBuffer) {
         pos += mBuffer->getOriginalBufferSize();
     }
@@ -77,7 +92,7 @@
         mBigramPos = NOT_A_DICT_POS;
     }
     // Update siblingPos if needed.
-    if (mSiblingPos == NOT_A_VALID_WORD_POS) {
+    if (mSiblingPos == NOT_A_DICT_POS) {
         // Sibling position is the tail position of current node.
         mSiblingPos = pos;
     }
@@ -88,4 +103,19 @@
     }
 }
 
+void DynamicPatriciaTrieNodeReader::invalidatePtNodeInfo() {
+    mHeadPos = NOT_A_DICT_POS;
+    mFlags = 0;
+    mParentPos = NOT_A_DICT_POS;
+    mCodePointCount = 0;
+    mProbabilityFieldPos = NOT_A_DICT_POS;
+    mProbability = NOT_A_PROBABILITY;
+    mChildrenPosFieldPos = NOT_A_DICT_POS;
+    mChildrenPos = NOT_A_DICT_POS;
+    mBigramLinkedNodePos = NOT_A_DICT_POS;
+    mShortcutPos = NOT_A_DICT_POS;
+    mBigramPos = NOT_A_DICT_POS;
+    mSiblingPos = NOT_A_DICT_POS;
+}
+
 }
diff --git a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_node_reader.h b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_node_reader.h
index 2ee7c24..6ef5f58 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_node_reader.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_node_reader.h
@@ -39,12 +39,12 @@
             const DictionaryBigramsStructurePolicy *const bigramsPolicy,
             const DictionaryShortcutsStructurePolicy *const shortcutsPolicy)
             : mBuffer(buffer), mBigramsPolicy(bigramsPolicy),
-              mShortcutsPolicy(shortcutsPolicy), mNodePos(NOT_A_VALID_WORD_POS), mFlags(0),
-              mParentPos(NOT_A_DICT_POS),  mCodePointCount(0),
-              mProbabilityFieldPos(NOT_A_DICT_POS), mProbability(NOT_A_PROBABILITY),
-              mChildrenPosFieldPos(NOT_A_DICT_POS), mChildrenPos(NOT_A_DICT_POS),
-              mShortcutPos(NOT_A_DICT_POS), mBigramPos(NOT_A_DICT_POS),
-              mSiblingPos(NOT_A_VALID_WORD_POS) {}
+              mShortcutsPolicy(shortcutsPolicy), mHeadPos(NOT_A_DICT_POS), mFlags(0),
+              mParentPos(NOT_A_DICT_POS), mCodePointCount(0), mProbabilityFieldPos(NOT_A_DICT_POS),
+              mProbability(NOT_A_PROBABILITY), mChildrenPosFieldPos(NOT_A_DICT_POS),
+              mChildrenPos(NOT_A_DICT_POS), mBigramLinkedNodePos(NOT_A_DICT_POS),
+              mShortcutPos(NOT_A_DICT_POS),  mBigramPos(NOT_A_DICT_POS),
+              mSiblingPos(NOT_A_DICT_POS) {}
 
     ~DynamicPatriciaTrieNodeReader() {}
 
@@ -56,13 +56,14 @@
 
     AK_FORCE_INLINE void fetchNodeInfoFromBufferAndGetNodeCodePoints(const int nodePos,
             const int maxCodePointCount, int *const outCodePoints) {
-        mNodePos = nodePos;
-        mSiblingPos = NOT_A_VALID_WORD_POS;
-        fetchNodeInfoFromBufferAndProcessMovedNode(mNodePos, maxCodePointCount, outCodePoints);
+        mSiblingPos = NOT_A_DICT_POS;
+        mBigramLinkedNodePos = NOT_A_DICT_POS;
+        fetchNodeInfoFromBufferAndProcessMovedNode(nodePos, maxCodePointCount, outCodePoints);
     }
 
-    AK_FORCE_INLINE int getNodePos() const {
-        return mNodePos;
+    // HeadPos is different from NodePos when the current PtNode is a moved PtNode.
+    AK_FORCE_INLINE int getHeadPos() const {
+        return mHeadPos;
     }
 
     // Flags
@@ -114,6 +115,11 @@
         return mChildrenPos;
     }
 
+    // Bigram linked node position.
+    AK_FORCE_INLINE int getBigramLinkedNodePos() const {
+        return mBigramLinkedNodePos;
+    }
+
     // Shortcutlist position
     AK_FORCE_INLINE int getShortcutPos() const {
         return mShortcutPos;
@@ -135,7 +141,7 @@
     const BufferWithExtendableBuffer *const mBuffer;
     const DictionaryBigramsStructurePolicy *const mBigramsPolicy;
     const DictionaryShortcutsStructurePolicy *const mShortcutsPolicy;
-    int mNodePos;
+    int mHeadPos;
     DynamicPatriciaTrieReadingUtils::NodeFlags mFlags;
     int mParentPos;
     uint8_t mCodePointCount;
@@ -143,12 +149,15 @@
     int mProbability;
     int mChildrenPosFieldPos;
     int mChildrenPos;
+    int mBigramLinkedNodePos;
     int mShortcutPos;
     int mBigramPos;
     int mSiblingPos;
 
     void fetchNodeInfoFromBufferAndProcessMovedNode(const int nodePos, const int maxCodePointCount,
             int *const outCodePoints);
+
+    void invalidatePtNodeInfo();
 };
 } // namespace latinime
 #endif /* LATINIME_DYNAMIC_PATRICIA_TRIE_NODE_READER_H */
diff --git a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_policy.cpp b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_policy.cpp
index 945677b..3cfbfd8 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_policy.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_policy.cpp
@@ -38,7 +38,7 @@
     readingHelper.initWithNodeArrayPos(dicNode->getChildrenPos());
     const DynamicPatriciaTrieNodeReader *const nodeReader = readingHelper.getNodeReader();
     while (!readingHelper.isEnd()) {
-        childDicNodes->pushLeavingChild(dicNode, nodeReader->getNodePos(),
+        childDicNodes->pushLeavingChild(dicNode, nodeReader->getHeadPos(),
                 nodeReader->getChildrenPos(), nodeReader->getProbability(),
                 nodeReader->isTerminal() && !nodeReader->isDeleted(),
                 nodeReader->hasChildren(), nodeReader->isBlacklisted() || nodeReader->isNotAWord(),
@@ -116,23 +116,23 @@
             if (!readingHelper.isMatchedCodePoint(
                     j, searchCodePoints[matchedCodePointCount + j])) {
                 // Different code point is found. The given word is not included in the dictionary.
-                return NOT_A_VALID_WORD_POS;
+                return NOT_A_DICT_POS;
             }
         }
         // All characters are matched.
         if (length == readingHelper.getTotalCodePointCount()) {
             // Terminal position is found.
-            return nodeReader->getNodePos();
+            return nodeReader->getHeadPos();
         }
         if (!nodeReader->hasChildren()) {
-            return NOT_A_VALID_WORD_POS;
+            return NOT_A_DICT_POS;
         }
         // Advance to the children nodes.
         readingHelper.readChildNode();
     }
     // If we already traversed the tree further than the word is long, there means
     // there was no match (or we would have found it).
-    return NOT_A_VALID_WORD_POS;
+    return NOT_A_DICT_POS;
 }
 
 int DynamicPatriciaTriePolicy::getProbability(const int unigramProbability,
@@ -149,7 +149,7 @@
 }
 
 int DynamicPatriciaTriePolicy::getUnigramProbabilityOfPtNode(const int nodePos) const {
-    if (nodePos == NOT_A_VALID_WORD_POS) {
+    if (nodePos == NOT_A_DICT_POS) {
         return NOT_A_PROBABILITY;
     }
     DynamicPatriciaTrieNodeReader nodeReader(&mBufferWithExtendableBuffer,
@@ -162,7 +162,7 @@
 }
 
 int DynamicPatriciaTriePolicy::getShortcutPositionOfNode(const int nodePos) const {
-    if (nodePos == NOT_A_VALID_WORD_POS) {
+    if (nodePos == NOT_A_DICT_POS) {
         return NOT_A_DICT_POS;
     }
     DynamicPatriciaTrieNodeReader nodeReader(&mBufferWithExtendableBuffer,
@@ -175,7 +175,7 @@
 }
 
 int DynamicPatriciaTriePolicy::getBigramsPositionOfNode(const int nodePos) const {
-    if (nodePos == NOT_A_VALID_WORD_POS) {
+    if (nodePos == NOT_A_DICT_POS) {
         return NOT_A_DICT_POS;
     }
     DynamicPatriciaTrieNodeReader nodeReader(&mBufferWithExtendableBuffer,
@@ -209,12 +209,12 @@
     }
     const int word0Pos = getTerminalNodePositionOfWord(word0, length0,
             false /* forceLowerCaseSearch */);
-    if (word0Pos == NOT_A_VALID_WORD_POS) {
+    if (word0Pos == NOT_A_DICT_POS) {
         return false;
     }
     const int word1Pos = getTerminalNodePositionOfWord(word1, length1,
             false /* forceLowerCaseSearch */);
-    if (word1Pos == NOT_A_VALID_WORD_POS) {
+    if (word1Pos == NOT_A_DICT_POS) {
         return false;
     }
     DynamicPatriciaTrieWritingHelper writingHelper(&mBufferWithExtendableBuffer,
@@ -230,12 +230,12 @@
     }
     const int word0Pos = getTerminalNodePositionOfWord(word0, length0,
             false /* forceLowerCaseSearch */);
-    if (word0Pos == NOT_A_VALID_WORD_POS) {
+    if (word0Pos == NOT_A_DICT_POS) {
         return false;
     }
     const int word1Pos = getTerminalNodePositionOfWord(word1, length1,
             false /* forceLowerCaseSearch */);
-    if (word1Pos == NOT_A_VALID_WORD_POS) {
+    if (word1Pos == NOT_A_DICT_POS) {
         return false;
     }
     DynamicPatriciaTrieWritingHelper writingHelper(&mBufferWithExtendableBuffer,
@@ -243,4 +243,29 @@
     return writingHelper.removeBigramWords(word0Pos, word1Pos);
 }
 
+void DynamicPatriciaTriePolicy::flush(const char *const filePath) {
+    if (!mBuffer->isUpdatable()) {
+        AKLOGI("Warning: flush() is called for non-updatable dictionary.");
+        return;
+    }
+    // TODO: Implement.
+}
+
+void DynamicPatriciaTriePolicy::flushWithGC(const char *const filePath) {
+    if (!mBuffer->isUpdatable()) {
+        AKLOGI("Warning: flushWithGC() is called for non-updatable dictionary.");
+        return;
+    }
+    // TODO: Implement.
+}
+
+bool DynamicPatriciaTriePolicy::needsToRunGC() const {
+    if (!mBuffer->isUpdatable()) {
+        AKLOGI("Warning: needsToRunGC() is called for non-updatable dictionary.");
+        return false;
+    }
+    // TODO: Implement.
+    return false;
+}
+
 } // namespace latinime
diff --git a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_policy.h b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_policy.h
index cdab0e1..2cbb0ff 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_policy.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_policy.h
@@ -36,8 +36,8 @@
             : mBuffer(buffer), mHeaderPolicy(mBuffer->getBuffer()),
               mBufferWithExtendableBuffer(mBuffer->getBuffer() + mHeaderPolicy.getSize(),
                       mBuffer->getBufferSize() - mHeaderPolicy.getSize()),
-              mBigramListPolicy(&mBufferWithExtendableBuffer),
-              mShortcutListPolicy(&mBufferWithExtendableBuffer) {}
+              mShortcutListPolicy(&mBufferWithExtendableBuffer),
+              mBigramListPolicy(&mBufferWithExtendableBuffer, &mShortcutListPolicy) {}
 
     ~DynamicPatriciaTriePolicy() {
         delete mBuffer;
@@ -85,14 +85,20 @@
     bool removeBigramWords(const int *const word0, const int length0, const int *const word1,
             const int length1);
 
+    void flush(const char *const filePath);
+
+    void flushWithGC(const char *const filePath);
+
+    bool needsToRunGC() const;
+
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(DynamicPatriciaTriePolicy);
 
     const MmappedBuffer *const mBuffer;
     const HeaderPolicy mHeaderPolicy;
     BufferWithExtendableBuffer mBufferWithExtendableBuffer;
-    DynamicBigramListPolicy mBigramListPolicy;
     DynamicShortcutListPolicy mShortcutListPolicy;
+    DynamicBigramListPolicy mBigramListPolicy;
 };
 } // namespace latinime
 #endif // LATINIME_DYNAMIC_PATRICIA_TRIE_POLICY_H
diff --git a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_reading_helper.h b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_reading_helper.h
index db1c392..120fd76 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_reading_helper.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_reading_helper.h
@@ -72,8 +72,7 @@
 
     // Initialize reading state with the head position of a node.
     AK_FORCE_INLINE void initWithNodePos(const int nodePos) {
-        // TODO: Consolidate NOT_A_VALID_WORD_POS and NOT_A_DICT_POS
-        if (nodePos == NOT_A_VALID_WORD_POS || nodePos == NOT_A_DICT_POS) {
+        if (nodePos == NOT_A_DICT_POS) {
             mPos = NOT_A_DICT_POS;
         } else {
             mIsError = false;
diff --git a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_reading_utils.cpp b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_reading_utils.cpp
index 5d979fa..8428c0b 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_reading_utils.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_reading_utils.cpp
@@ -28,19 +28,26 @@
 const DptReadingUtils::NodeFlags DptReadingUtils::FLAG_IS_MOVED = 0x40;
 const DptReadingUtils::NodeFlags DptReadingUtils::FLAG_IS_DELETED = 0x80;
 
+/* static */ int DptReadingUtils::getForwardLinkPosition(const uint8_t *const buffer,
+        const int pos) {
+    int linkAddressPos = pos;
+    return ByteArrayUtils::readSint24AndAdvancePosition(buffer, &linkAddressPos);
+}
+
+/* static */ int DptReadingUtils::getParentPosAndAdvancePosition(const uint8_t *const buffer,
+        int *const pos) {
+    return ByteArrayUtils::readSint24AndAdvancePosition(buffer, pos);
+}
+
 /* static */ int DptReadingUtils::readChildrenPositionAndAdvancePosition(
-        const uint8_t *const buffer, const NodeFlags flags, int *const pos) {
-    if ((flags & MASK_MOVED) == FLAG_IS_NOT_MOVED) {
-        const int base = *pos;
-        const int offset = ByteArrayUtils::readSint24AndAdvancePosition(buffer, pos);
-        if (offset == 0) {
-            // 0 offset means that the node does not have children.
-            return NOT_A_DICT_POS;
-        } else {
-            return base + offset;
-        }
-    } else {
+        const uint8_t *const buffer, int *const pos) {
+    const int base = *pos;
+    const int offset = ByteArrayUtils::readSint24AndAdvancePosition(buffer, pos);
+    if (offset == 0) {
+        // 0 offset means that the node does not have children.
         return NOT_A_DICT_POS;
+    } else {
+        return base + offset;
     }
 }
 
diff --git a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_reading_utils.h b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_reading_utils.h
index 62d73bb..db5f9b1 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_reading_utils.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_reading_utils.h
@@ -20,7 +20,6 @@
 #include <stdint.h>
 
 #include "defines.h"
-#include "suggest/policyimpl/dictionary/utils/byte_array_utils.h"
 
 namespace latinime {
 
@@ -28,22 +27,15 @@
  public:
     typedef uint8_t NodeFlags;
 
-    static AK_FORCE_INLINE int getForwardLinkPosition(const uint8_t *const buffer, const int pos) {
-        int linkAddressPos = pos;
-        return ByteArrayUtils::readSint24AndAdvancePosition(buffer, &linkAddressPos);
-    }
+    static int getForwardLinkPosition(const uint8_t *const buffer, const int pos);
 
     static AK_FORCE_INLINE bool isValidForwardLinkPosition(const int forwardLinkAddress) {
         return forwardLinkAddress != 0;
     }
 
-    static AK_FORCE_INLINE int getParentPosAndAdvancePosition(const uint8_t *const buffer,
-            int *const pos) {
-        return ByteArrayUtils::readSint24AndAdvancePosition(buffer, pos);
-    }
+    static int getParentPosAndAdvancePosition(const uint8_t *const buffer, int *const pos);
 
-    static int readChildrenPositionAndAdvancePosition(const uint8_t *const buffer,
-            const NodeFlags flags, int *const pos);
+    static int readChildrenPositionAndAdvancePosition(const uint8_t *const buffer, int *const pos);
 
     /**
      * Node Flags
@@ -59,9 +51,9 @@
     static AK_FORCE_INLINE NodeFlags updateAndGetFlags(const NodeFlags originalFlags,
             const bool isMoved, const bool isDeleted) {
         NodeFlags flags = originalFlags;
-        flags = isMoved ? ((flags & (!MASK_MOVED)) | FLAG_IS_MOVED) : flags;
-        flags = isDeleted ? ((flags & (!MASK_MOVED)) | FLAG_IS_DELETED) : flags;
-        flags = (!isMoved && !isDeleted) ? ((flags & (!MASK_MOVED)) | FLAG_IS_NOT_MOVED) : flags;
+        flags = isMoved ? ((flags & (~MASK_MOVED)) | FLAG_IS_MOVED) : flags;
+        flags = isDeleted ? ((flags & (~MASK_MOVED)) | FLAG_IS_DELETED) : flags;
+        flags = (!isMoved && !isDeleted) ? ((flags & (~MASK_MOVED)) | FLAG_IS_NOT_MOVED) : flags;
         return flags;
     }
 
diff --git a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_writing_helper.cpp b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_writing_helper.cpp
index 99a983f..311d31e 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_writing_helper.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_writing_helper.cpp
@@ -26,10 +26,12 @@
 
 namespace latinime {
 
+const int DynamicPatriciaTrieWritingHelper::CHILDREN_POSITION_FIELD_SIZE = 3;
+
 bool DynamicPatriciaTrieWritingHelper::addUnigramWord(
         DynamicPatriciaTrieReadingHelper *const readingHelper,
         const int *const wordCodePoints, const int codePointCount, const int probability) {
-    int parentPos = NOT_A_VALID_WORD_POS;
+    int parentPos = NOT_A_DICT_POS;
     while (!readingHelper->isEnd()) {
         const int matchedCodePointCount = readingHelper->getPrevTotalCodePointCount();
         if (!readingHelper->isMatchedCodePoint(0 /* index */,
@@ -63,7 +65,7 @@
                     codePointCount - readingHelper->getTotalCodePointCount());
         }
         // Advance to the children nodes.
-        parentPos = nodeReader->getNodePos();
+        parentPos = nodeReader->getHeadPos();
         readingHelper->readChildNode();
     }
     if (readingHelper->isError()) {
@@ -79,29 +81,60 @@
 
 bool DynamicPatriciaTrieWritingHelper::addBigramWords(const int word0Pos, const int word1Pos,
         const int probability) {
+    int mMergedNodeCodePoints[MAX_WORD_LENGTH];
     DynamicPatriciaTrieNodeReader nodeReader(mBuffer, mBigramPolicy, mShortcutPolicy);
-    nodeReader.fetchNodeInfoFromBuffer(word0Pos);
-    if (nodeReader.isDeleted()) {
+    nodeReader.fetchNodeInfoFromBufferAndGetNodeCodePoints(word0Pos, MAX_WORD_LENGTH,
+            mMergedNodeCodePoints);
+    // Move node to add bigram entry.
+    const int newNodePos = mBuffer->getTailPosition();
+    if (!markNodeAsMovedAndSetPosition(&nodeReader, newNodePos, newNodePos)) {
         return false;
     }
-    // TODO: Implement.
-    return false;
+    int writingPos = newNodePos;
+    // Write a new PtNode using original PtNode's info to the tail of the dictionary.
+    if (!writePtNodeToBufferByCopyingPtNodeInfo(&nodeReader, nodeReader.getParentPos(),
+            mMergedNodeCodePoints, nodeReader.getCodePointCount(), nodeReader.getProbability(),
+            &writingPos)) {
+        return false;
+    }
+    nodeReader.fetchNodeInfoFromBuffer(newNodePos);
+    if (nodeReader.getBigramsPos() != NOT_A_DICT_POS) {
+        // Insert a new bigram entry into the existing bigram list.
+        int bigramListPos = nodeReader.getBigramsPos();
+        return mBigramPolicy->addNewBigramEntryToBigramList(word1Pos, probability, &bigramListPos);
+    } else {
+        // The PtNode doesn't have a bigram list.
+        // First, Write a bigram entry at the tail position of the PtNode.
+        if (!mBigramPolicy->writeNewBigramEntry(word1Pos, probability, &writingPos)) {
+            return false;
+        }
+        // Then, Mark as the PtNode having bigram list in the flags.
+        const PatriciaTrieReadingUtils::NodeFlags updatedFlags =
+                PatriciaTrieReadingUtils::createAndGetFlags(nodeReader.isBlacklisted(),
+                        nodeReader.isNotAWord(), nodeReader.getProbability() != NOT_A_PROBABILITY,
+                        nodeReader.getShortcutPos() != NOT_A_DICT_POS, true /* hasBigrams */,
+                        nodeReader.getCodePointCount() > 1, CHILDREN_POSITION_FIELD_SIZE);
+        writingPos = newNodePos;
+        // Write updated flags into the moved PtNode's flags field.
+        return DynamicPatriciaTrieWritingUtils::writeFlagsAndAdvancePosition(mBuffer, updatedFlags,
+                &writingPos);
+    }
 }
 
 // Remove a bigram relation from word0Pos to word1Pos.
 bool DynamicPatriciaTrieWritingHelper::removeBigramWords(const int word0Pos, const int word1Pos) {
     DynamicPatriciaTrieNodeReader nodeReader(mBuffer, mBigramPolicy, mShortcutPolicy);
     nodeReader.fetchNodeInfoFromBuffer(word0Pos);
-    if (nodeReader.isDeleted() || nodeReader.getBigramsPos() == NOT_A_DICT_POS) {
+    if (nodeReader.getBigramsPos() == NOT_A_DICT_POS) {
         return false;
     }
-    // TODO: Implement.
-    return false;
+    return mBigramPolicy->removeBigram(nodeReader.getBigramsPos(), word1Pos);
 }
 
 bool DynamicPatriciaTrieWritingHelper::markNodeAsMovedAndSetPosition(
-        const DynamicPatriciaTrieNodeReader *const originalNode, const int movedPos) {
-    int pos = originalNode->getNodePos();
+        const DynamicPatriciaTrieNodeReader *const originalNode, const int movedPos,
+        const int bigramLinkedNodePos) {
+    int pos = originalNode->getHeadPos();
     const bool usesAdditionalBuffer = mBuffer->isInAdditionalBuffer(pos);
     const uint8_t *const dictBuf = mBuffer->getBuffer(usesAdditionalBuffer);
     if (usesAdditionalBuffer) {
@@ -113,18 +146,42 @@
     const PatriciaTrieReadingUtils::NodeFlags updatedFlags =
             DynamicPatriciaTrieReadingUtils::updateAndGetFlags(originalFlags, true /* isMoved */,
                     false /* isDeleted */);
-    int writingPos = originalNode->getNodePos();
+    int writingPos = originalNode->getHeadPos();
     // Update flags.
     if (!DynamicPatriciaTrieWritingUtils::writeFlagsAndAdvancePosition(mBuffer, updatedFlags,
             &writingPos)) {
         return false;
     }
     // Update moved position, which is stored in the parent offset field.
-    const int movedPosOffset = movedPos - originalNode->getNodePos();
+    const int movedPosOffset = movedPos - originalNode->getHeadPos();
     if (!DynamicPatriciaTrieWritingUtils::writeParentOffsetAndAdvancePosition(
             mBuffer, movedPosOffset, &writingPos)) {
         return false;
     }
+    // Update bigram linked node position, which is stored in the children position field.
+    int childrenPosFieldPos = originalNode->getChildrenPosFieldPos();
+    if (!DynamicPatriciaTrieWritingUtils::writeChildrenPositionAndAdvancePosition(
+            mBuffer, bigramLinkedNodePos, &childrenPosFieldPos)) {
+        return false;
+    }
+    if (originalNode->hasChildren()) {
+        // Update children's parent position.
+        DynamicPatriciaTrieReadingHelper readingHelper(mBuffer, mBigramPolicy, mShortcutPolicy);
+        const DynamicPatriciaTrieNodeReader *const nodeReader = readingHelper.getNodeReader();
+        readingHelper.initWithNodeArrayPos(originalNode->getChildrenPos());
+        while (!readingHelper.isEnd()) {
+            const int childPtNodeWrittenPos = nodeReader->getHeadPos();
+            const int parentOffset = movedPos - childPtNodeWrittenPos;
+            int parentOffsetFieldPos = childPtNodeWrittenPos + 1 /* Flags */;
+            if (!DynamicPatriciaTrieWritingUtils::writeParentOffsetAndAdvancePosition(
+                    mBuffer, parentOffset, &parentOffsetFieldPos)) {
+                // Parent offset cannot be written because of a bug or a broken dictionary; thus,
+                // we give up to update dictionary.
+                return false;
+            }
+            readingHelper.readNextSiblingNode();
+        }
+    }
     return true;
 }
 
@@ -135,13 +192,9 @@
         const int originalBigramListPos, const int originalShortcutListPos,
         int *const writingPos) {
     const int nodePos = *writingPos;
-    // Create node flags and write them.
-    const PatriciaTrieReadingUtils::NodeFlags nodeFlags =
-            PatriciaTrieReadingUtils::createAndGetFlags(isBlacklisted, isNotAWord,
-                    probability != NOT_A_PROBABILITY, originalShortcutListPos != NOT_A_DICT_POS,
-                    originalBigramListPos != NOT_A_DICT_POS, codePointCount > 1,
-                    3 /* childrenPositionFieldSize */);
-    if (!DynamicPatriciaTrieWritingUtils::writeFlagsAndAdvancePosition(mBuffer, nodeFlags,
+    // Write dummy flags. The Node flags are updated with appropriate flags at the last step of the
+    // PtNode writing.
+    if (!DynamicPatriciaTrieWritingUtils::writeFlagsAndAdvancePosition(mBuffer, 0 /* nodeFlags */,
             writingPos)) {
         return false;
     }
@@ -154,7 +207,7 @@
     // Write code points
     if (!DynamicPatriciaTrieWritingUtils::writeCodePointsAndAdvancePosition(mBuffer, codePoints,
             codePointCount, writingPos)) {
-        return false;;
+        return false;
     }
     // Write probability when the probability is a valid probability, which means this node is
     // terminal.
@@ -177,12 +230,25 @@
         }
     }
     // Copy bigram list when the originalBigramListPos is valid dictionary position.
+    int bigramCount = 0;
     if (originalBigramListPos != NOT_A_DICT_POS) {
         int fromPos = originalBigramListPos;
-        if (!mBigramPolicy->copyAllBigrams(&fromPos, writingPos)) {
+        if (!mBigramPolicy->copyAllBigrams(&fromPos, writingPos, &bigramCount)) {
             return false;
         }
     }
+    // Create node flags and write them.
+    PatriciaTrieReadingUtils::NodeFlags nodeFlags =
+            PatriciaTrieReadingUtils::createAndGetFlags(isBlacklisted, isNotAWord,
+                    probability != NOT_A_PROBABILITY /* isTerminal */,
+                    originalShortcutListPos != NOT_A_DICT_POS /* hasShortcutTargets */,
+                    bigramCount > 0 /* hasBigrams */, codePointCount > 1 /* hasMultipleChars */,
+                    CHILDREN_POSITION_FIELD_SIZE);
+    int flagsFieldPos = nodePos;
+    if (!DynamicPatriciaTrieWritingUtils::writeFlagsAndAdvancePosition(mBuffer, nodeFlags,
+            &flagsFieldPos)) {
+        return false;
+    }
     return true;
 }
 
@@ -230,7 +296,7 @@
     } else {
         // Make the node terminal and write the probability.
         int movedPos = mBuffer->getTailPosition();
-        if (!markNodeAsMovedAndSetPosition(originalPtNode, movedPos)) {
+        if (!markNodeAsMovedAndSetPosition(originalPtNode, movedPos, movedPos)) {
             return false;
         }
         if (!writePtNodeToBufferByCopyingPtNodeInfo(originalPtNode, originalPtNode->getParentPos(),
@@ -250,7 +316,7 @@
             newPtNodeArrayPos, &childrenPosFieldPos)) {
         return false;
     }
-    return createNewPtNodeArrayWithAChildPtNode(parentNode->getNodePos(), codePoints,
+    return createNewPtNodeArrayWithAChildPtNode(parentNode->getHeadPos(), codePoints,
             codePointCount, probability);
 }
 
@@ -287,11 +353,8 @@
     // Reallocating PtNode: abcde, newNode: abc.
     // abc (1st, terminal) __ de (2nd)
     const bool addsExtraChild = newNodeCodePointCount > overlappingCodePointCount;
-    const int firstPtNodePos = mBuffer->getTailPosition();
-    if (!markNodeAsMovedAndSetPosition(reallocatingPtNode, firstPtNodePos)) {
-        return false;
-    }
-    int writingPos = firstPtNodePos;
+    const int firstPartOfReallocatedPtNodePos = mBuffer->getTailPosition();
+    int writingPos = firstPartOfReallocatedPtNodePos;
     // Write the 1st part of the reallocating node. The children position will be updated later
     // with actual children position.
     const int newProbability = addsExtraChild ? NOT_A_PROBABILITY : probabilityOfNewPtNode;
@@ -307,15 +370,15 @@
         return false;
     }
     // Write the 2nd part of the reallocating node.
-    if (!writePtNodeToBufferByCopyingPtNodeInfo(reallocatingPtNode,
-            reallocatingPtNode->getNodePos(),
+    const int secondPartOfReallocatedPtNodePos = writingPos;
+    if (!writePtNodeToBufferByCopyingPtNodeInfo(reallocatingPtNode, firstPartOfReallocatedPtNodePos,
             reallocatingPtNodeCodePoints + overlappingCodePointCount,
             reallocatingPtNode->getCodePointCount() - overlappingCodePointCount,
             reallocatingPtNode->getProbability(), &writingPos)) {
         return false;
     }
     if (addsExtraChild) {
-        if (!writePtNodeToBuffer(reallocatingPtNode->getNodePos(),
+        if (!writePtNodeToBuffer(firstPartOfReallocatedPtNodePos,
                 newNodeCodePoints + overlappingCodePointCount,
                 newNodeCodePointCount - overlappingCodePointCount, probabilityOfNewPtNode,
                 &writingPos)) {
@@ -326,9 +389,14 @@
             NOT_A_DICT_POS /* forwardLinkPos */, &writingPos)) {
         return false;
     }
+    // Update original reallocatingPtNode as moved.
+    if (!markNodeAsMovedAndSetPosition(reallocatingPtNode, firstPartOfReallocatedPtNodePos,
+            secondPartOfReallocatedPtNodePos)) {
+        return false;
+    }
     // Load node info. Information of the 1st part will be fetched.
     DynamicPatriciaTrieNodeReader nodeReader(mBuffer, mBigramPolicy, mShortcutPolicy);
-    nodeReader.fetchNodeInfoFromBuffer(firstPtNodePos);
+    nodeReader.fetchNodeInfoFromBuffer(firstPartOfReallocatedPtNodePos);
     // Update children position.
     int childrenPosFieldPos = nodeReader.getChildrenPosFieldPos();
     if (!DynamicPatriciaTrieWritingUtils::writeChildrenPositionAndAdvancePosition(mBuffer,
diff --git a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_writing_helper.h b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_writing_helper.h
index ada634a..20e35ab 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_writing_helper.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_writing_helper.h
@@ -49,12 +49,14 @@
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(DynamicPatriciaTrieWritingHelper);
 
+    static const int CHILDREN_POSITION_FIELD_SIZE;
+
     BufferWithExtendableBuffer *const mBuffer;
     DynamicBigramListPolicy *const mBigramPolicy;
     DynamicShortcutListPolicy *const mShortcutPolicy;
 
     bool markNodeAsMovedAndSetPosition(const DynamicPatriciaTrieNodeReader *const nodeToUpdate,
-            const int movedPos);
+            const int movedPos, const int bigramLinkedNodePos);
 
     bool writePtNodeWithFullInfoToBuffer(const bool isBlacklisted, const bool isNotAWord,
             const int parentPos,  const int *const codePoints, const int codePointCount,
diff --git a/native/jni/src/suggest/policyimpl/dictionary/patricia_trie_policy.cpp b/native/jni/src/suggest/policyimpl/dictionary/patricia_trie_policy.cpp
index d5a83a9..e6cff43 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/patricia_trie_policy.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/patricia_trie_policy.cpp
@@ -219,7 +219,7 @@
 }
 
 // This function gets the position of the terminal node of the exact matching word in the
-// dictionary. If no match is found, it returns NOT_A_VALID_WORD_POS.
+// dictionary. If no match is found, it returns NOT_A_DICT_POS.
 int PatriciaTriePolicy::getTerminalNodePositionOfWord(const int *const inWord,
         const int length, const bool forceLowerCaseSearch) const {
     int pos = getRootPosition();
@@ -228,7 +228,7 @@
     while (true) {
         // If we already traversed the tree further than the word is long, there means
         // there was no match (or we would have found it).
-        if (wordPos >= length) return NOT_A_VALID_WORD_POS;
+        if (wordPos >= length) return NOT_A_DICT_POS;
         int ptNodeCount = PatriciaTrieReadingUtils::getPtNodeArraySizeAndAdvancePosition(mDictRoot,
                 &pos);
         const int wChar = forceLowerCaseSearch
@@ -236,7 +236,7 @@
         while (true) {
             // If there are no more PtNodes in this array, it means we could not
             // find a matching character for this depth, therefore there is no match.
-            if (0 >= ptNodeCount) return NOT_A_VALID_WORD_POS;
+            if (0 >= ptNodeCount) return NOT_A_DICT_POS;
             const int ptNodePos = pos;
             const PatriciaTrieReadingUtils::NodeFlags flags =
                     PatriciaTrieReadingUtils::getFlagsAndAdvancePosition(mDictRoot, &pos);
@@ -245,7 +245,7 @@
             if (character == wChar) {
                 // This is the correct PtNode. Only one PtNode may start with the same char within
                 // a PtNode array, so either we found our match in this array, or there is
-                // no match and we can return NOT_A_VALID_WORD_POS. So we will check all the
+                // no match and we can return NOT_A_DICT_POS. So we will check all the
                 // characters in this PtNode indeed does match.
                 if (PatriciaTrieReadingUtils::hasMultipleChars(flags)) {
                     character = PatriciaTrieReadingUtils::getCodePointAndAdvancePosition(mDictRoot,
@@ -256,8 +256,8 @@
                         // character that does not match, as explained above, it means the word is
                         // not in the dictionary (by virtue of this PtNode being the only one to
                         // match the word on the first character, but not matching the whole word).
-                        if (wordPos >= length) return NOT_A_VALID_WORD_POS;
-                        if (inWord[wordPos] != character) return NOT_A_VALID_WORD_POS;
+                        if (wordPos >= length) return NOT_A_DICT_POS;
+                        if (inWord[wordPos] != character) return NOT_A_DICT_POS;
                         character = PatriciaTrieReadingUtils::getCodePointAndAdvancePosition(
                                 mDictRoot, &pos);
                     }
@@ -274,7 +274,7 @@
                     PatriciaTrieReadingUtils::readProbabilityAndAdvancePosition(mDictRoot, &pos);
                 }
                 if (!PatriciaTrieReadingUtils::hasChildrenInFlags(flags)) {
-                    return NOT_A_VALID_WORD_POS;
+                    return NOT_A_DICT_POS;
                 }
                 // We have children and we are still shorter than the word we are searching for, so
                 // we need to traverse children. Put the pointer on the children position, and
@@ -320,7 +320,7 @@
 }
 
 int PatriciaTriePolicy::getUnigramProbabilityOfPtNode(const int nodePos) const {
-    if (nodePos == NOT_A_VALID_WORD_POS) {
+    if (nodePos == NOT_A_DICT_POS) {
         return NOT_A_PROBABILITY;
     }
     int pos = nodePos;
@@ -342,7 +342,7 @@
 }
 
 int PatriciaTriePolicy::getShortcutPositionOfNode(const int nodePos) const {
-    if (nodePos == NOT_A_VALID_WORD_POS) {
+    if (nodePos == NOT_A_DICT_POS) {
         return NOT_A_DICT_POS;
     }
     int pos = nodePos;
@@ -362,7 +362,7 @@
 }
 
 int PatriciaTriePolicy::getBigramsPositionOfNode(const int nodePos) const {
-    if (nodePos == NOT_A_VALID_WORD_POS) {
+    if (nodePos == NOT_A_DICT_POS) {
         return NOT_A_DICT_POS;
     }
     int pos = nodePos;
diff --git a/native/jni/src/suggest/policyimpl/dictionary/patricia_trie_policy.h b/native/jni/src/suggest/policyimpl/dictionary/patricia_trie_policy.h
index 75d9762..cee3e4a 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/patricia_trie_policy.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/patricia_trie_policy.h
@@ -96,6 +96,22 @@
         return false;
     }
 
+    void flush(const char *const filePath) {
+        // This method should not be called for non-updatable dictionary.
+        AKLOGI("Warning: flush() is called for non-updatable dictionary.");
+    }
+
+    void flushWithGC(const char *const filePath) {
+        // This method should not be called for non-updatable dictionary.
+        AKLOGI("Warning: flushWithGC() is called for non-updatable dictionary.");
+    }
+
+    bool needsToRunGC() const {
+        // This method should not be called for non-updatable dictionary.
+        AKLOGI("Warning: needsToRunGC() is called for non-updatable dictionary.");
+        return false;
+    }
+
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(PatriciaTriePolicy);
 
diff --git a/native/jni/src/suggest/policyimpl/dictionary/patricia_trie_reading_utils.cpp b/native/jni/src/suggest/policyimpl/dictionary/patricia_trie_reading_utils.cpp
index 576a158..1316b42 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/patricia_trie_reading_utils.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/patricia_trie_reading_utils.cpp
@@ -42,6 +42,63 @@
 // Flag for blacklist
 const PtReadingUtils::NodeFlags PtReadingUtils::FLAG_IS_BLACKLISTED = 0x01;
 
+/* static */ int PtReadingUtils::getPtNodeArraySizeAndAdvancePosition(
+        const uint8_t *const buffer, int *const pos) {
+    const uint8_t firstByte = ByteArrayUtils::readUint8AndAdvancePosition(buffer, pos);
+    if (firstByte < 0x80) {
+        return firstByte;
+    } else {
+        return ((firstByte & 0x7F) << 8) ^ ByteArrayUtils::readUint8AndAdvancePosition(
+                buffer, pos);
+    }
+}
+
+/* static */ PtReadingUtils::NodeFlags PtReadingUtils::getFlagsAndAdvancePosition(
+        const uint8_t *const buffer, int *const pos) {
+    return ByteArrayUtils::readUint8AndAdvancePosition(buffer, pos);
+}
+
+/* static */ int PtReadingUtils::getCodePointAndAdvancePosition(const uint8_t *const buffer,
+        int *const pos) {
+    return ByteArrayUtils::readCodePointAndAdvancePosition(buffer, pos);
+}
+
+// Returns the number of read characters.
+/* static */ int PtReadingUtils::getCharsAndAdvancePosition(const uint8_t *const buffer,
+        const NodeFlags flags, const int maxLength, int *const outBuffer, int *const pos) {
+    int length = 0;
+    if (hasMultipleChars(flags)) {
+        length = ByteArrayUtils::readStringAndAdvancePosition(buffer, maxLength, outBuffer,
+                pos);
+    } else {
+        if (maxLength > 0) {
+            outBuffer[0] = getCodePointAndAdvancePosition(buffer, pos);
+            length = 1;
+        }
+    }
+    return length;
+}
+
+// Returns the number of skipped characters.
+/* static */ int PtReadingUtils::skipCharacters(const uint8_t *const buffer, const NodeFlags flags,
+        const int maxLength, int *const pos) {
+    if (hasMultipleChars(flags)) {
+        return ByteArrayUtils::advancePositionToBehindString(buffer, maxLength, pos);
+    } else {
+        if (maxLength > 0) {
+            getCodePointAndAdvancePosition(buffer, pos);
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+}
+
+/* static */ int PtReadingUtils::readProbabilityAndAdvancePosition(const uint8_t *const buffer,
+        int *const pos) {
+    return ByteArrayUtils::readUint8AndAdvancePosition(buffer, pos);
+}
+
 /* static */ int PtReadingUtils::readChildrenPositionAndAdvancePosition(
         const uint8_t *const buffer, const NodeFlags flags, int *const pos) {
     const int base = *pos;
diff --git a/native/jni/src/suggest/policyimpl/dictionary/patricia_trie_reading_utils.h b/native/jni/src/suggest/policyimpl/dictionary/patricia_trie_reading_utils.h
index 2b0646d..8420ee9 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/patricia_trie_reading_utils.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/patricia_trie_reading_utils.h
@@ -20,7 +20,6 @@
 #include <stdint.h>
 
 #include "defines.h"
-#include "suggest/policyimpl/dictionary/utils/byte_array_utils.h"
 
 namespace latinime {
 
@@ -28,62 +27,21 @@
  public:
     typedef uint8_t NodeFlags;
 
-    static AK_FORCE_INLINE int getPtNodeArraySizeAndAdvancePosition(
-            const uint8_t *const buffer, int *const pos) {
-        const uint8_t firstByte = ByteArrayUtils::readUint8AndAdvancePosition(buffer, pos);
-        if (firstByte < 0x80) {
-            return firstByte;
-        } else {
-            return ((firstByte & 0x7F) << 8) ^ ByteArrayUtils::readUint8AndAdvancePosition(
-                    buffer, pos);
-        }
-    }
+    static int getPtNodeArraySizeAndAdvancePosition(const uint8_t *const buffer, int *const pos);
 
-    static AK_FORCE_INLINE NodeFlags getFlagsAndAdvancePosition(const uint8_t *const buffer,
-            int *const pos) {
-        return ByteArrayUtils::readUint8AndAdvancePosition(buffer, pos);
-    }
+    static NodeFlags getFlagsAndAdvancePosition(const uint8_t *const buffer, int *const pos);
 
-    static AK_FORCE_INLINE int getCodePointAndAdvancePosition(const uint8_t *const buffer,
-            int *const pos) {
-        return ByteArrayUtils::readCodePointAndAdvancePosition(buffer, pos);
-    }
+    static int getCodePointAndAdvancePosition(const uint8_t *const buffer, int *const pos);
 
     // Returns the number of read characters.
-    static AK_FORCE_INLINE int getCharsAndAdvancePosition(const uint8_t *const buffer,
-            const NodeFlags flags, const int maxLength, int *const outBuffer, int *const pos) {
-        int length = 0;
-        if (hasMultipleChars(flags)) {
-            length = ByteArrayUtils::readStringAndAdvancePosition(buffer, maxLength, outBuffer,
-                    pos);
-        } else {
-            if (maxLength > 0) {
-                outBuffer[0] = getCodePointAndAdvancePosition(buffer, pos);
-                length = 1;
-            }
-        }
-        return length;
-    }
+    static int getCharsAndAdvancePosition(const uint8_t *const buffer, const NodeFlags flags,
+            const int maxLength, int *const outBuffer, int *const pos);
 
     // Returns the number of skipped characters.
-    static AK_FORCE_INLINE int skipCharacters(const uint8_t *const buffer, const NodeFlags flags,
-            const int maxLength, int *const pos) {
-        if (hasMultipleChars(flags)) {
-            return ByteArrayUtils::advancePositionToBehindString(buffer, maxLength, pos);
-        } else {
-            if (maxLength > 0) {
-                getCodePointAndAdvancePosition(buffer, pos);
-                return 1;
-            } else {
-                return 0;
-            }
-        }
-    }
+    static int skipCharacters(const uint8_t *const buffer, const NodeFlags flags,
+            const int maxLength, int *const pos);
 
-    static AK_FORCE_INLINE int readProbabilityAndAdvancePosition(const uint8_t *const buffer,
-            int *const pos) {
-        return ByteArrayUtils::readUint8AndAdvancePosition(buffer, pos);
-    }
+    static int readProbabilityAndAdvancePosition(const uint8_t *const buffer, int *const pos);
 
     static int readChildrenPositionAndAdvancePosition(const uint8_t *const buffer,
             const NodeFlags flags, int *const pos);
diff --git a/native/jni/src/suggest/policyimpl/dictionary/shortcut/shortcut_list_reading_utils.cpp b/native/jni/src/suggest/policyimpl/dictionary/shortcut/shortcut_list_reading_utils.cpp
index e70bb50..847dcde 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/shortcut/shortcut_list_reading_utils.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/shortcut/shortcut_list_reading_utils.cpp
@@ -16,6 +16,8 @@
 
 #include "suggest/policyimpl/dictionary/shortcut/shortcut_list_reading_utils.h"
 
+#include "suggest/policyimpl/dictionary/utils/byte_array_utils.h"
+
 namespace latinime {
 
 // Flag for presence of more attributes
@@ -28,4 +30,22 @@
 // The numeric value of the shortcut probability that means 'whitelist'.
 const int ShortcutListReadingUtils::WHITELIST_SHORTCUT_PROBABILITY = 15;
 
+/* static */ ShortcutListReadingUtils::ShortcutFlags
+        ShortcutListReadingUtils::getFlagsAndForwardPointer(const uint8_t *const dictRoot,
+                int *const pos) {
+    return ByteArrayUtils::readUint8AndAdvancePosition(dictRoot, pos);
+}
+
+/* static */ int ShortcutListReadingUtils::getShortcutListSizeAndForwardPointer(
+        const uint8_t *const dictRoot, int *const pos) {
+    // readUint16andAdvancePosition() returns an offset *including* the uint16 field itself.
+    return ByteArrayUtils::readUint16AndAdvancePosition(dictRoot, pos)
+            - SHORTCUT_LIST_SIZE_FIELD_SIZE;
+}
+
+/* static */ int ShortcutListReadingUtils::readShortcutTarget(
+        const uint8_t *const dictRoot, const int maxLength,  int *const outWord, int *const pos) {
+    return ByteArrayUtils::readStringAndAdvancePosition(dictRoot, maxLength, outWord, pos);
+}
+
 } // namespace latinime
diff --git a/native/jni/src/suggest/policyimpl/dictionary/shortcut/shortcut_list_reading_utils.h b/native/jni/src/suggest/policyimpl/dictionary/shortcut/shortcut_list_reading_utils.h
index 5f4f240..a83ed5a 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/shortcut/shortcut_list_reading_utils.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/shortcut/shortcut_list_reading_utils.h
@@ -20,7 +20,6 @@
 #include <stdint.h>
 
 #include "defines.h"
-#include "suggest/policyimpl/dictionary/utils/byte_array_utils.h"
 
 namespace latinime {
 
@@ -28,10 +27,7 @@
  public:
     typedef uint8_t ShortcutFlags;
 
-    static AK_FORCE_INLINE ShortcutFlags getFlagsAndForwardPointer(
-            const uint8_t *const dictRoot, int *const pos) {
-        return ByteArrayUtils::readUint8AndAdvancePosition(dictRoot, pos);
-    }
+    static ShortcutFlags getFlagsAndForwardPointer(const uint8_t *const dictRoot, int *const pos);
 
     static AK_FORCE_INLINE int getProbabilityFromFlags(const ShortcutFlags flags) {
         return flags & MASK_ATTRIBUTE_PROBABILITY;
@@ -43,12 +39,7 @@
 
     // This method returns the size of the shortcut list region excluding the shortcut list size
     // field at the beginning.
-    static AK_FORCE_INLINE int getShortcutListSizeAndForwardPointer(
-            const uint8_t *const dictRoot, int *const pos) {
-        // readUint16andAdvancePosition() returns an offset *including* the uint16 field itself.
-        return ByteArrayUtils::readUint16AndAdvancePosition(dictRoot, pos)
-                - SHORTCUT_LIST_SIZE_FIELD_SIZE;
-    }
+    static int getShortcutListSizeAndForwardPointer(const uint8_t *const dictRoot, int *const pos);
 
     static AK_FORCE_INLINE int getShortcutListSizeFieldSize() {
         return SHORTCUT_LIST_SIZE_FIELD_SIZE;
@@ -63,11 +54,8 @@
         return getProbabilityFromFlags(flags) == WHITELIST_SHORTCUT_PROBABILITY;
     }
 
-    static AK_FORCE_INLINE int readShortcutTarget(
-            const uint8_t *const dictRoot, const int maxLength,  int *const outWord,
-            int *const pos) {
-        return ByteArrayUtils::readStringAndAdvancePosition(dictRoot, maxLength, outWord, pos);
-    }
+    static int readShortcutTarget(const uint8_t *const dictRoot, const int maxLength,
+            int *const outWord, int *const pos);
 
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(ShortcutListReadingUtils);
diff --git a/native/jni/src/suggest/policyimpl/dictionary/utils/buffer_with_extendable_buffer.cpp b/native/jni/src/suggest/policyimpl/dictionary/utils/buffer_with_extendable_buffer.cpp
index dfdaebd..0fed275 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/utils/buffer_with_extendable_buffer.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/utils/buffer_with_extendable_buffer.cpp
@@ -76,7 +76,7 @@
                 }
             }
             mUsedAdditionalBufferSize += size;
-        } else if (pos + size >= tailPosition) {
+        } else if (pos + size > tailPosition) {
             // The access will beyond the tail of used region.
             return false;
         }
diff --git a/native/jni/src/suggest/policyimpl/dictionary/utils/byte_array_utils.cpp b/native/jni/src/suggest/policyimpl/dictionary/utils/byte_array_utils.cpp
index a84cfb9..1833e88 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/utils/byte_array_utils.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/utils/byte_array_utils.cpp
@@ -18,7 +18,8 @@
 
 namespace latinime {
 
-const uint8_t ByteArrayUtils::MINIMAL_ONE_BYTE_CHARACTER_VALUE = 0x20;
+const uint8_t ByteArrayUtils::MINIMUM_ONE_BYTE_CHARACTER_VALUE = 0x20;
+const uint8_t ByteArrayUtils::MAXIMUM_ONE_BYTE_CHARACTER_VALUE = 0xFF;
 const uint8_t ByteArrayUtils::CHARACTER_ARRAY_TERMINATOR = 0x1F;
 
 } // namespace latinime
diff --git a/native/jni/src/suggest/policyimpl/dictionary/utils/byte_array_utils.h b/native/jni/src/suggest/policyimpl/dictionary/utils/byte_array_utils.h
index f727ecf..0c15768 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/utils/byte_array_utils.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/utils/byte_array_utils.h
@@ -135,7 +135,7 @@
     static AK_FORCE_INLINE int readCodePointAndAdvancePosition(
             const uint8_t *const buffer, int *const pos) {
         const uint8_t firstByte = readUint8(buffer, *pos);
-        if (firstByte < MINIMAL_ONE_BYTE_CHARACTER_VALUE) {
+        if (firstByte < MINIMUM_ONE_BYTE_CHARACTER_VALUE) {
             if (firstByte == CHARACTER_ARRAY_TERMINATOR) {
                 *pos += 1;
                 return NOT_A_CODE_POINT;
@@ -172,6 +172,7 @@
         int codePoint = readCodePointAndAdvancePosition(buffer, pos);
         while (NOT_A_CODE_POINT != codePoint && length < maxLength) {
             codePoint = readCodePointAndAdvancePosition(buffer, pos);
+            length++;
         }
         return length;
     }
@@ -186,7 +187,8 @@
             const int codePoint = codePoints[i];
             if (codePoint == NOT_A_CODE_POINT || codePoint == CHARACTER_ARRAY_TERMINATOR) {
                 break;
-            } else if (codePoint < MINIMAL_ONE_BYTE_CHARACTER_VALUE) {
+            } else if (codePoint < MINIMUM_ONE_BYTE_CHARACTER_VALUE
+                    || codePoint > MAXIMUM_ONE_BYTE_CHARACTER_VALUE) {
                 // three bytes character.
                 writeUint24AndAdvancePosition(buffer, codePoint, pos);
             } else {
@@ -206,7 +208,8 @@
             const int codePoint = codePoints[i];
             if (codePoint == NOT_A_CODE_POINT || codePoint == CHARACTER_ARRAY_TERMINATOR) {
                 break;
-            } else if (codePoint < MINIMAL_ONE_BYTE_CHARACTER_VALUE) {
+            } else if (codePoint < MINIMUM_ONE_BYTE_CHARACTER_VALUE
+                    || codePoint > MAXIMUM_ONE_BYTE_CHARACTER_VALUE) {
                 // three bytes character.
                 byteCount += 3;
             } else {
@@ -224,7 +227,8 @@
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(ByteArrayUtils);
 
-    static const uint8_t MINIMAL_ONE_BYTE_CHARACTER_VALUE;
+    static const uint8_t MINIMUM_ONE_BYTE_CHARACTER_VALUE;
+    static const uint8_t MAXIMUM_ONE_BYTE_CHARACTER_VALUE;
     static const uint8_t CHARACTER_ARRAY_TERMINATOR;
 
     static AK_FORCE_INLINE void writeUint32AndAdvancePosition(uint8_t *const buffer,
diff --git a/native/jni/src/suggest/policyimpl/dictionary/utils/mmapped_buffer.h b/native/jni/src/suggest/policyimpl/dictionary/utils/mmapped_buffer.h
index 6febd78..6b69116 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/utils/mmapped_buffer.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/utils/mmapped_buffer.h
@@ -29,8 +29,8 @@
 
 class MmappedBuffer {
  public:
-    static MmappedBuffer* openBuffer(const char *const path, const int pathLength,
-            const int bufferOffset, const int bufferSize, const bool isUpdatable) {
+    static MmappedBuffer* openBuffer(const char *const path, const int bufferOffset,
+            const int bufferSize, const bool isUpdatable) {
         const int openMode = isUpdatable ? O_RDWR : O_RDONLY;
         const int mmapFd = open(path, openMode);
         if (mmapFd < 0) {
diff --git a/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java b/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
index e2e1489..4d231cd 100644
--- a/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
+++ b/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
@@ -19,6 +19,7 @@
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.LargeTest;
 
+import com.android.inputmethod.latin.makedict.CodePointUtils;
 import com.android.inputmethod.latin.makedict.DictEncoder;
 import com.android.inputmethod.latin.makedict.FormatSpec;
 import com.android.inputmethod.latin.makedict.FusionDictionary;
@@ -28,8 +29,10 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Locale;
+import java.util.Random;
 
 @LargeTest
 public class BinaryDictionaryTests extends AndroidTestCase {
@@ -84,4 +87,216 @@
                 binaryDictionary.isValidDictionary());
         binaryDictionary.close();
     }
+
+    public void testAddUnigramWord() {
+        File dictFile = null;
+        try {
+            dictFile = createEmptyDictionaryAndGetFile("TestBinaryDictionary");
+        } catch (IOException e) {
+            fail("IOException while writing an initial dictionary : " + e);
+        } catch (UnsupportedFormatException e) {
+            fail("UnsupportedFormatException while writing an initial dictionary : " + e);
+        }
+        BinaryDictionary binaryDictionary = new BinaryDictionary(dictFile.getAbsolutePath(),
+                0 /* offset */, dictFile.length(), true /* useFullEditDistance */,
+                Locale.getDefault(), TEST_LOCALE, true /* isUpdatable */);
+
+        final int probability = 100;
+        binaryDictionary.addUnigramWord("aaa", probability);
+        // Reallocate and create.
+        binaryDictionary.addUnigramWord("aab", probability);
+        // Insert into children.
+        binaryDictionary.addUnigramWord("aac", probability);
+        // Make terminal.
+        binaryDictionary.addUnigramWord("aa", probability);
+        // Create children.
+        binaryDictionary.addUnigramWord("aaaa", probability);
+        // Reallocate and make termianl.
+        binaryDictionary.addUnigramWord("a", probability);
+
+        final int updatedProbability = 200;
+        // Update.
+        binaryDictionary.addUnigramWord("aaa", updatedProbability);
+
+        assertEquals(probability, binaryDictionary.getFrequency("aab"));
+        assertEquals(probability, binaryDictionary.getFrequency("aac"));
+        assertEquals(probability, binaryDictionary.getFrequency("aa"));
+        assertEquals(probability, binaryDictionary.getFrequency("aaaa"));
+        assertEquals(probability, binaryDictionary.getFrequency("a"));
+        assertEquals(updatedProbability, binaryDictionary.getFrequency("aaa"));
+
+        dictFile.delete();
+    }
+
+    public void testRandomlyAddUnigramWord() {
+        final int wordCount = 1000;
+        final int codePointSetSize = 50;
+        final int seed = 123456789;
+
+        File dictFile = null;
+        try {
+            dictFile = createEmptyDictionaryAndGetFile("TestBinaryDictionary");
+        } catch (IOException e) {
+            fail("IOException while writing an initial dictionary : " + e);
+        } catch (UnsupportedFormatException e) {
+            fail("UnsupportedFormatException while writing an initial dictionary : " + e);
+        }
+        BinaryDictionary binaryDictionary = new BinaryDictionary(dictFile.getAbsolutePath(),
+                0 /* offset */, dictFile.length(), true /* useFullEditDistance */,
+                Locale.getDefault(), TEST_LOCALE, true /* isUpdatable */);
+
+        final HashMap<String, Integer> probabilityMap = new HashMap<String, Integer>();
+        // Test a word that isn't contained within the dictionary.
+        final Random random = new Random(seed);
+        final int[] codePointSet = CodePointUtils.generateCodePointSet(codePointSetSize, random);
+        for (int i = 0; i < wordCount; ++i) {
+            final String word = CodePointUtils.generateWord(random, codePointSet);
+            probabilityMap.put(word, random.nextInt() & 0xFF);
+        }
+        for (String word : probabilityMap.keySet()) {
+            binaryDictionary.addUnigramWord(word, probabilityMap.get(word));
+        }
+        for (String word : probabilityMap.keySet()) {
+            assertEquals(word, (int)probabilityMap.get(word), binaryDictionary.getFrequency(word));
+        }
+        dictFile.delete();
+    }
+
+    public void testAddBigramWords() {
+        // TODO: Add a test to check the frequency of the bigram score which uses current value
+        // calculated in the native code
+        File dictFile = null;
+        try {
+            dictFile = createEmptyDictionaryAndGetFile("TestBinaryDictionary");
+        } catch (IOException e) {
+            fail("IOException while writing an initial dictionary : " + e);
+        } catch (UnsupportedFormatException e) {
+            fail("UnsupportedFormatException while writing an initial dictionary : " + e);
+        }
+        BinaryDictionary binaryDictionary = new BinaryDictionary(dictFile.getAbsolutePath(),
+                0 /* offset */, dictFile.length(), true /* useFullEditDistance */,
+                Locale.getDefault(), TEST_LOCALE, true /* isUpdatable */);
+
+        final int unigramProbability = 100;
+        final int bigramProbability = 10;
+        binaryDictionary.addUnigramWord("aaa", unigramProbability);
+        binaryDictionary.addUnigramWord("abb", unigramProbability);
+        binaryDictionary.addUnigramWord("bcc", unigramProbability);
+        binaryDictionary.addBigramWords("aaa", "abb", bigramProbability);
+        binaryDictionary.addBigramWords("aaa", "bcc", bigramProbability);
+        binaryDictionary.addBigramWords("abb", "aaa", bigramProbability);
+        binaryDictionary.addBigramWords("abb", "bcc", bigramProbability);
+
+        assertEquals(true, binaryDictionary.isValidBigram("aaa", "abb"));
+        assertEquals(true, binaryDictionary.isValidBigram("aaa", "bcc"));
+        assertEquals(true, binaryDictionary.isValidBigram("abb", "aaa"));
+        assertEquals(true, binaryDictionary.isValidBigram("abb", "bcc"));
+
+        assertEquals(false, binaryDictionary.isValidBigram("bcc", "aaa"));
+        assertEquals(false, binaryDictionary.isValidBigram("bcc", "bbc"));
+        assertEquals(false, binaryDictionary.isValidBigram("aaa", "aaa"));
+
+        dictFile.delete();
+    }
+
+    public void testRandomlyAddBigramWords() {
+        // TODO: Add a test to check the frequency of the bigram score which uses current value
+        // calculated in the native code
+        final int wordCount = 100;
+        final int bigramCount = 1000;
+        final int codePointSetSize = 50;
+        final int seed = 11111;
+
+        File dictFile = null;
+        try {
+            dictFile = createEmptyDictionaryAndGetFile("TestBinaryDictionary");
+        } catch (IOException e) {
+            fail("IOException while writing an initial dictionary : " + e);
+        } catch (UnsupportedFormatException e) {
+            fail("UnsupportedFormatException while writing an initial dictionary : " + e);
+        }
+        BinaryDictionary binaryDictionary = new BinaryDictionary(dictFile.getAbsolutePath(),
+                0 /* offset */, dictFile.length(), true /* useFullEditDistance */,
+                Locale.getDefault(), TEST_LOCALE, true /* isUpdatable */);
+        final ArrayList<String> words = new ArrayList<String>();
+        // Test a word that isn't contained within the dictionary.
+        final Random random = new Random(seed);
+        final int[] codePointSet = CodePointUtils.generateCodePointSet(codePointSetSize, random);
+        final int unigramProbability = 100;
+        final int bigramProbability = 10;
+        for (int i = 0; i < wordCount; ++i) {
+            final String word = CodePointUtils.generateWord(random, codePointSet);
+            words.add(word);
+            binaryDictionary.addUnigramWord(word, unigramProbability);
+        }
+
+        final boolean[][] bigramRelations = new boolean[wordCount][wordCount];
+        for (int i = 0; i < bigramCount; i++) {
+            final int word0Index = random.nextInt(wordCount);
+            final int word1Index = random.nextInt(wordCount);
+            final String word0 = words.get(word0Index);
+            final String word1 = words.get(word1Index);
+
+            bigramRelations[word0Index][word1Index] = true;
+            binaryDictionary.addBigramWords(word0, word1, bigramProbability);
+        }
+
+        for (int i = 0; i < words.size(); i++) {
+            for (int j = 0; j < words.size(); j++) {
+                assertEquals(bigramRelations[i][j],
+                        binaryDictionary.isValidBigram(words.get(i), words.get(j)));
+            }
+        }
+
+        dictFile.delete();
+    }
+
+    public void testRemoveBigramWords() {
+        File dictFile = null;
+        try {
+            dictFile = createEmptyDictionaryAndGetFile("TestBinaryDictionary");
+        } catch (IOException e) {
+            fail("IOException while writing an initial dictionary : " + e);
+        } catch (UnsupportedFormatException e) {
+            fail("UnsupportedFormatException while writing an initial dictionary : " + e);
+        }
+        BinaryDictionary binaryDictionary = new BinaryDictionary(dictFile.getAbsolutePath(),
+                0 /* offset */, dictFile.length(), true /* useFullEditDistance */,
+                Locale.getDefault(), TEST_LOCALE, true /* isUpdatable */);
+
+        final int unigramProbability = 100;
+        final int bigramProbability = 10;
+        binaryDictionary.addUnigramWord("aaa", unigramProbability);
+        binaryDictionary.addUnigramWord("abb", unigramProbability);
+        binaryDictionary.addUnigramWord("bcc", unigramProbability);
+        binaryDictionary.addBigramWords("aaa", "abb", bigramProbability);
+        binaryDictionary.addBigramWords("aaa", "bcc", bigramProbability);
+        binaryDictionary.addBigramWords("abb", "aaa", bigramProbability);
+        binaryDictionary.addBigramWords("abb", "bcc", bigramProbability);
+
+        assertEquals(true, binaryDictionary.isValidBigram("aaa", "abb"));
+        assertEquals(true, binaryDictionary.isValidBigram("aaa", "bcc"));
+        assertEquals(true, binaryDictionary.isValidBigram("abb", "aaa"));
+        assertEquals(true, binaryDictionary.isValidBigram("abb", "bcc"));
+
+        binaryDictionary.removeBigramWords("aaa", "abb");
+        assertEquals(false, binaryDictionary.isValidBigram("aaa", "abb"));
+        binaryDictionary.addBigramWords("aaa", "abb", bigramProbability);
+        assertEquals(true, binaryDictionary.isValidBigram("aaa", "abb"));
+
+
+        binaryDictionary.removeBigramWords("aaa", "bcc");
+        assertEquals(false, binaryDictionary.isValidBigram("aaa", "bcc"));
+        binaryDictionary.removeBigramWords("abb", "aaa");
+        assertEquals(false, binaryDictionary.isValidBigram("abb", "aaa"));
+        binaryDictionary.removeBigramWords("abb", "bcc");
+        assertEquals(false, binaryDictionary.isValidBigram("abb", "bcc"));
+
+        binaryDictionary.removeBigramWords("aaa", "abb");
+        // Test remove non-existing bigram operation.
+        binaryDictionary.removeBigramWords("aaa", "abb");
+        binaryDictionary.removeBigramWords("bcc", "aaa");
+
+        dictFile.delete();
+    }
 }
diff --git a/tests/src/com/android/inputmethod/latin/InputPointersTests.java b/tests/src/com/android/inputmethod/latin/InputPointersTests.java
index f0b6acc..5095f96 100644
--- a/tests/src/com/android/inputmethod/latin/InputPointersTests.java
+++ b/tests/src/com/android/inputmethod/latin/InputPointersTests.java
@@ -244,4 +244,20 @@
                     expecteds[i + expectedPos], actuals[i + actualPos]);
         }
     }
+
+    public void testShift() {
+        final InputPointers src = new InputPointers(DEFAULT_CAPACITY);
+        final int limit = 100;
+        final int shiftAmount = 20;
+        for (int i = 0; i < limit; i++) {
+            src.addPointer(i, i * 2, i * 3, i * 4);
+        }
+        src.shift(shiftAmount);
+        for (int i = 0; i < limit - shiftAmount; ++i) {
+            assertEquals("xCoordinates at " + i, i + shiftAmount, src.getXCoordinates()[i]);
+            assertEquals("yCoordinates at " + i, (i + shiftAmount) * 2, src.getYCoordinates()[i]);
+            assertEquals("pointerIds at " + i, (i + shiftAmount) * 3, src.getPointerIds()[i]);
+            assertEquals("times at " + i, (i + shiftAmount) * 4, src.getTimes()[i]);
+        }
+    }
 }
diff --git a/tests/src/com/android/inputmethod/latin/InputTestsBase.java b/tests/src/com/android/inputmethod/latin/InputTestsBase.java
index 2603b35..234bb1b 100644
--- a/tests/src/com/android/inputmethod/latin/InputTestsBase.java
+++ b/tests/src/com/android/inputmethod/latin/InputTestsBase.java
@@ -259,7 +259,8 @@
     protected void pickSuggestionManually(final int index, final String suggestion) {
         mLatinIME.pickSuggestionManually(index, new SuggestedWordInfo(suggestion, 1,
                 SuggestedWordInfo.KIND_CORRECTION, null /* sourceDict */,
-                SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */));
+                SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+                SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
     }
 
     // Helper to avoid writing the try{}catch block each time
diff --git a/tests/src/com/android/inputmethod/latin/SuggestedWordsTests.java b/tests/src/com/android/inputmethod/latin/SuggestedWordsTests.java
index a5f3685..4cf8333 100644
--- a/tests/src/com/android/inputmethod/latin/SuggestedWordsTests.java
+++ b/tests/src/com/android/inputmethod/latin/SuggestedWordsTests.java
@@ -35,11 +35,13 @@
         final ArrayList<SuggestedWordInfo> list = CollectionUtils.newArrayList();
         list.add(new SuggestedWordInfo(TYPED_WORD, TYPED_WORD_FREQ,
                 SuggestedWordInfo.KIND_TYPED, null /* sourceDict */,
-                SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */));
+                SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+                SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
         for (int i = 0; i < NUMBER_OF_ADDED_SUGGESTIONS; ++i) {
             list.add(new SuggestedWordInfo("" + i, 1, SuggestedWordInfo.KIND_CORRECTION,
                     null /* sourceDict */,
-                    SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */));
+                    SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+                    SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
         }
 
         final SuggestedWords words = new SuggestedWords(
diff --git a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
index 72ec5a3..8bc0095 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
@@ -25,6 +25,7 @@
 import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding;
 import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer;
 import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
+import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions;
 import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode;
 import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray;
 import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
@@ -86,7 +87,8 @@
         Log.e(TAG, "Testing dictionary: seed is " + seed);
         final Random random = new Random(seed);
         sWords.clear();
-        final int[] codePointSet = generateCodePointSet(DEFAULT_CODE_POINT_SET_SIZE, random);
+        final int[] codePointSet = CodePointUtils.generateCodePointSet(DEFAULT_CODE_POINT_SET_SIZE,
+                random);
         generateWords(maxUnigrams, random, codePointSet);
 
         for (int i = 0; i < sWords.size(); ++i) {
@@ -112,63 +114,10 @@
         }
     }
 
-    private int[] generateCodePointSet(final int codePointSetSize, final Random random) {
-        final int[] codePointSet = new int[codePointSetSize];
-        for (int i = codePointSet.length - 1; i >= 0; ) {
-            final int r = Math.abs(random.nextInt());
-            if (r < 0) continue;
-            // Don't insert 0~0x20, but insert any other code point.
-            // Code points are in the range 0~0x10FFFF.
-            final int candidateCodePoint = 0x20 + r % (Character.MAX_CODE_POINT - 0x20);
-            // Code points between MIN_ and MAX_SURROGATE are not valid on their own.
-            if (candidateCodePoint >= Character.MIN_SURROGATE
-                    && candidateCodePoint <= Character.MAX_SURROGATE) continue;
-            codePointSet[i] = candidateCodePoint;
-            --i;
-        }
-        return codePointSet;
-    }
-
-    // Utilities for test
-
-    /**
-     * Makes new DictDecoder according to BUFFER_TYPE.
-     */
-    private Ver3DictDecoder getDictDecoder(final File file, final int bufferType) {
-        if (bufferType == USE_BYTE_BUFFER) {
-            return new Ver3DictDecoder(file, DictDecoder.USE_READONLY_BYTEBUFFER);
-        } else  if (bufferType == USE_BYTE_ARRAY) {
-            return new Ver3DictDecoder(file, DictDecoder.USE_BYTEARRAY);
-        }
-        return null;
-    }
-
-    /**
-     * Generates a random word.
-     */
-    private String generateWord(final Random random, final int[] codePointSet) {
-        StringBuilder builder = new StringBuilder();
-        // 8 * 4 = 32 chars max, but we do it the following way so as to bias the random toward
-        // longer words. This should be closer to natural language, and more importantly, it will
-        // exercise the algorithms in dicttool much more.
-        final int count = 1 + (Math.abs(random.nextInt()) % 5)
-                + (Math.abs(random.nextInt()) % 5)
-                + (Math.abs(random.nextInt()) % 5)
-                + (Math.abs(random.nextInt()) % 5)
-                + (Math.abs(random.nextInt()) % 5)
-                + (Math.abs(random.nextInt()) % 5)
-                + (Math.abs(random.nextInt()) % 5)
-                + (Math.abs(random.nextInt()) % 5);
-        while (builder.length() < count) {
-            builder.appendCodePoint(codePointSet[Math.abs(random.nextInt()) % codePointSet.length]);
-        }
-        return builder.toString();
-    }
-
     private void generateWords(final int number, final Random random, final int[] codePointSet) {
         final Set<String> wordSet = CollectionUtils.newHashSet();
         while (wordSet.size() < number) {
-            wordSet.add(generateWord(random, codePointSet));
+            wordSet.add(CodePointUtils.generateWord(random, codePointSet));
         }
         sWords.addAll(wordSet);
     }
@@ -276,6 +225,27 @@
         return result + ", supportsDynamicUpdate = " + formatOptions.mSupportsDynamicUpdate;
     }
 
+    private DictionaryOptions getDictionaryOptions(final String id, final String version) {
+        final DictionaryOptions options = new DictionaryOptions(new HashMap<String, String>(),
+                false, false);
+        options.mAttributes.put("version", version);
+        options.mAttributes.put("dictionary", id);
+        return options;
+    }
+
+    private File setUpDictionaryFile(final String name, final String version) {
+        File file = null;
+        try {
+            file = new File(getContext().getCacheDir(), name + "." + version
+                    + TEST_DICT_FILE_EXTENSION);
+            file.createNewFile();
+        } catch (IOException e) {
+            // do nothing
+        }
+        assertTrue("Failed to create the dictionary file.", file.exists());
+        return file;
+    }
+
     // Tests for readDictionaryBinary and writeDictionaryBinary
 
     private long timeReadingAndCheckDict(final File file, final List<String> words,
@@ -285,11 +255,9 @@
 
         FusionDictionary dict = null;
         try {
-            final Ver3DictDecoder dictDecoder = getDictDecoder(file, bufferType);
-            dictDecoder.openDictBuffer();
-            assertNotNull(dictDecoder.getDictBuffer());
+            final DictDecoder dictDecoder = FormatSpec.getDictDecoder(file, bufferType);
             now = System.currentTimeMillis();
-            dict = dictDecoder.readDictionaryBinary(null);
+            dict = dictDecoder.readDictionaryBinary(null, false /* deleteDictIfBroken */);
             diff  = System.currentTimeMillis() - now;
         } catch (IOException e) {
             Log.e(TAG, "IOException while reading dictionary", e);
@@ -306,17 +274,13 @@
             final SparseArray<List<Integer>> bigrams, final HashMap<String, List<String>> shortcuts,
             final int bufferType, final FormatSpec.FormatOptions formatOptions,
             final String message) {
-        File file = null;
-        try {
-            file = File.createTempFile("runReadAndWrite", TEST_DICT_FILE_EXTENSION,
-                    getContext().getCacheDir());
-        } catch (IOException e) {
-            Log.e(TAG, "IOException", e);
-        }
-        assertNotNull(file);
+
+        final String dictName = "runReadAndWrite";
+        final String dictVersion = Long.toString(System.currentTimeMillis());
+        final File file = setUpDictionaryFile(dictName, dictVersion);
 
         final FusionDictionary dict = new FusionDictionary(new PtNodeArray(),
-                new FusionDictionary.DictionaryOptions(new HashMap<String,String>(), false, false));
+                getDictionaryOptions(dictName, dictVersion));
         addUnigrams(words.size(), dict, words, shortcuts);
         addBigrams(dict, words, bigrams);
         checkDictionary(dict, words, bigrams, shortcuts);
@@ -443,9 +407,7 @@
 
         long now = -1, diff = -1;
         try {
-            final Ver3DictDecoder dictDecoder = getDictDecoder(file, bufferType);
-            dictDecoder.openDictBuffer();
-            assertNotNull("Can't get buffer.", dictDecoder.getDictBuffer());
+            final DictDecoder dictDecoder = FormatSpec.getDictDecoder(file, bufferType);
             now = System.currentTimeMillis();
             dictDecoder.readUnigramsAndBigramsBinary(resultWords, resultFreqs, resultBigrams);
             diff = System.currentTimeMillis() - now;
@@ -470,19 +432,13 @@
     private String runReadUnigramsAndBigramsBinary(final ArrayList<String> words,
             final SparseArray<List<Integer>> bigrams, final int bufferType,
             final FormatSpec.FormatOptions formatOptions, final String message) {
-        File file = null;
-        try {
-            file = File.createTempFile("runReadUnigrams", TEST_DICT_FILE_EXTENSION,
-                    getContext().getCacheDir());
-        } catch (IOException e) {
-            Log.e(TAG, "IOException", e);
-        }
-        assertNotNull(file);
+        final String dictName = "runReadUnigrams";
+        final String dictVersion = Long.toString(System.currentTimeMillis());
+        final File file = setUpDictionaryFile(dictName, dictVersion);
 
         // making the dictionary from lists of words.
         final FusionDictionary dict = new FusionDictionary(new PtNodeArray(),
-                new FusionDictionary.DictionaryOptions(
-                        new HashMap<String, String>(), false, false));
+                getDictionaryOptions(dictName, dictVersion));
         addUnigrams(words.size(), dict, words, null /* shortcutMap */);
         addBigrams(dict, words, bigrams);
 
@@ -531,9 +487,8 @@
     }
 
     // Tests for getTerminalPosition
-    private String getWordFromBinary(final Ver3DictDecoder dictDecoder, final int address) {
-        final DictBuffer dictBuffer = dictDecoder.getDictBuffer();
-        if (dictBuffer.position() != 0) dictBuffer.position(0);
+    private String getWordFromBinary(final DictDecoder dictDecoder, final int address) {
+        if (dictDecoder.getPosition() != 0) dictDecoder.setPosition(0);
 
         FileHeader fileHeader = null;
         try {
@@ -548,7 +503,7 @@
                 address, fileHeader.mFormatOptions).mWord;
     }
 
-    private long runGetTerminalPosition(final Ver3DictDecoder dictDecoder, final String word,
+    private long runGetTerminalPosition(final DictDecoder dictDecoder, final String word,
             int index, boolean contained) {
         final int expectedFrequency = (UNIGRAM_FREQ + index) % 255;
         long diff = -1;
@@ -569,29 +524,23 @@
     }
 
     public void testGetTerminalPosition() {
-        File file = null;
-        try {
-            file = File.createTempFile("testGetTerminalPosition", TEST_DICT_FILE_EXTENSION,
-                    getContext().getCacheDir());
-        } catch (IOException e) {
-            // do nothing
-        }
-        assertNotNull(file);
+        final String dictName = "testGetTerminalPosition";
+        final String dictVersion = Long.toString(System.currentTimeMillis());
+        final File file = setUpDictionaryFile(dictName, dictVersion);
 
         final FusionDictionary dict = new FusionDictionary(new PtNodeArray(),
-                new FusionDictionary.DictionaryOptions(
-                        new HashMap<String, String>(), false, false));
+                getDictionaryOptions(dictName, dictVersion));
         addUnigrams(sWords.size(), dict, sWords, null /* shortcutMap */);
         timeWritingDictToFile(file, dict, VERSION3_WITH_DYNAMIC_UPDATE);
 
-        final Ver3DictDecoder dictDecoder = new Ver3DictDecoder(file, DictDecoder.USE_BYTEARRAY);
+        final DictDecoder dictDecoder = FormatSpec.getDictDecoder(file, DictDecoder.USE_BYTEARRAY);
         try {
             dictDecoder.openDictBuffer();
         } catch (IOException e) {
             // ignore
             Log.e(TAG, "IOException while opening the buffer", e);
         }
-        assertNotNull("Can't get the buffer", dictDecoder.getDictBuffer());
+        assertTrue("Can't get the buffer", dictDecoder.isOpenedDictBuffer());
 
         try {
             // too long word
@@ -617,23 +566,19 @@
 
         // Test a word that isn't contained within the dictionary.
         final Random random = new Random((int)System.currentTimeMillis());
-        final int[] codePointSet = generateCodePointSet(DEFAULT_CODE_POINT_SET_SIZE, random);
+        final int[] codePointSet = CodePointUtils.generateCodePointSet(DEFAULT_CODE_POINT_SET_SIZE,
+                random);
         for (int i = 0; i < 1000; ++i) {
-            final String word = generateWord(random, codePointSet);
+            final String word = CodePointUtils.generateWord(random, codePointSet);
             if (sWords.indexOf(word) != -1) continue;
             runGetTerminalPosition(dictDecoder, word, i, false);
         }
     }
 
     public void testDeleteWord() {
-        File file = null;
-        try {
-            file = File.createTempFile("testDeleteWord", TEST_DICT_FILE_EXTENSION,
-                    getContext().getCacheDir());
-        } catch (IOException e) {
-            // do nothing
-        }
-        assertNotNull(file);
+        final String dictName = "testDeleteWord";
+        final String dictVersion = Long.toString(System.currentTimeMillis());
+        final File file = setUpDictionaryFile(dictName, dictVersion);
 
         final FusionDictionary dict = new FusionDictionary(new PtNodeArray(),
                 new FusionDictionary.DictionaryOptions(
@@ -648,7 +593,7 @@
             // ignore
             Log.e(TAG, "IOException while opening the buffer", e);
         }
-        assertNotNull("Can't get the buffer", dictDecoder.getDictBuffer());
+        assertTrue("Can't get the buffer", dictDecoder.isOpenedDictBuffer());
 
         try {
             MoreAsserts.assertNotEqual(FormatSpec.NOT_VALID_WORD,
diff --git a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtilsTests.java b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtilsTests.java
index 8e0c6df..a837494 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtilsTests.java
@@ -140,7 +140,8 @@
         int position = FormatSpec.NOT_VALID_WORD;
 
         try {
-            final Ver3DictDecoder dictDecoder = new Ver3DictDecoder(file);
+            final Ver3DictDecoder dictDecoder = new Ver3DictDecoder(file,
+                    DictDecoder.USE_READONLY_BYTEBUFFER);
             position = dictDecoder.getTerminalPosition(word);
         } catch (IOException e) {
         } catch (UnsupportedFormatException e) {
@@ -149,7 +150,7 @@
     }
 
     /**
-     * Find a word using the Ver3DictDecoder.
+     * Find a word using the DictDecoder.
      *
      * @param dictDecoder the dict decoder
      * @param word the word searched
@@ -157,21 +158,20 @@
      * @throws IOException
      * @throws UnsupportedFormatException
      */
-    private static PtNodeInfo findWordByBinaryDictReader(final Ver3DictDecoder dictDecoder,
+    private static PtNodeInfo findWordByBinaryDictReader(final DictDecoder dictDecoder,
             final String word) throws IOException, UnsupportedFormatException {
         int position = dictDecoder.getTerminalPosition(word);
-        final DictBuffer dictBuffer = dictDecoder.getDictBuffer();
         if (position != FormatSpec.NOT_VALID_WORD) {
-            dictBuffer.position(0);
+            dictDecoder.setPosition(0);
             final FileHeader header = dictDecoder.readHeader();
-            dictBuffer.position(position);
+            dictDecoder.setPosition(position);
             return dictDecoder.readPtNode(position, header.mFormatOptions);
         }
         return null;
     }
 
     private PtNodeInfo findWordFromFile(final File file, final String word) {
-        final Ver3DictDecoder dictDecoder = new Ver3DictDecoder(file);
+        final DictDecoder dictDecoder = FormatSpec.getDictDecoder(file);
         PtNodeInfo info = null;
         try {
             dictDecoder.openDictBuffer();
@@ -234,7 +234,7 @@
     private void checkReverseLookup(final File file, final String word, final int position) {
 
         try {
-            final Ver3DictDecoder dictDecoder = new Ver3DictDecoder(file);
+            final DictDecoder dictDecoder = FormatSpec.getDictDecoder(file);
             final FileHeader fileHeader = dictDecoder.readHeader();
             assertEquals(word,
                     BinaryDictDecoderUtils.getWordAtPosition(dictDecoder, fileHeader.mHeaderSize,
diff --git a/tests/src/com/android/inputmethod/latin/makedict/CodePointUtils.java b/tests/src/com/android/inputmethod/latin/makedict/CodePointUtils.java
new file mode 100644
index 0000000..36b958a
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/makedict/CodePointUtils.java
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+
+package com.android.inputmethod.latin.makedict;
+
+import java.util.Random;
+
+// Utility methods related with code points used for tests.
+public class CodePointUtils {
+    private CodePointUtils() {
+        // This utility class is not publicly instantiable.
+    }
+
+    public static int[] generateCodePointSet(final int codePointSetSize, final Random random) {
+        final int[] codePointSet = new int[codePointSetSize];
+        for (int i = codePointSet.length - 1; i >= 0; ) {
+            final int r = Math.abs(random.nextInt());
+            if (r < 0) continue;
+            // Don't insert 0~0x20, but insert any other code point.
+            // Code points are in the range 0~0x10FFFF.
+            final int candidateCodePoint = 0x20 + r % (Character.MAX_CODE_POINT - 0x20);
+            // Code points between MIN_ and MAX_SURROGATE are not valid on their own.
+            if (candidateCodePoint >= Character.MIN_SURROGATE
+                    && candidateCodePoint <= Character.MAX_SURROGATE) continue;
+            codePointSet[i] = candidateCodePoint;
+            --i;
+        }
+        return codePointSet;
+    }
+
+    /**
+     * Generates a random word.
+     */
+    public static String generateWord(final Random random, final int[] codePointSet) {
+        StringBuilder builder = new StringBuilder();
+        // 8 * 4 = 32 chars max, but we do it the following way so as to bias the random toward
+        // longer words. This should be closer to natural language, and more importantly, it will
+        // exercise the algorithms in dicttool much more.
+        final int count = 1 + (Math.abs(random.nextInt()) % 5)
+                + (Math.abs(random.nextInt()) % 5)
+                + (Math.abs(random.nextInt()) % 5)
+                + (Math.abs(random.nextInt()) % 5)
+                + (Math.abs(random.nextInt()) % 5)
+                + (Math.abs(random.nextInt()) % 5)
+                + (Math.abs(random.nextInt()) % 5)
+                + (Math.abs(random.nextInt()) % 5);
+        while (builder.length() < count) {
+            builder.appendCodePoint(codePointSet[Math.abs(random.nextInt()) % codePointSet.length]);
+        }
+        return builder.toString();
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java b/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
index d15e88b..bf44a14 100644
--- a/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
+++ b/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
@@ -46,6 +46,7 @@
     };
 
     private static final int MIN_USER_HISTORY_DICTIONARY_FILE_SIZE = 1000;
+    private static final int WAIT_TERMINATING_IN_MILLISECONDS = 100;
 
     @Override
     public void setUp() {
@@ -122,8 +123,14 @@
                     true /* checksContents */);
         } finally {
             try {
+                final UserHistoryPredictionDictionary dict =
+                        PersonalizationHelper.getUserHistoryPredictionDictionary(getContext(),
+                                testFilenameSuffix, mPrefs);
                 Log.d(TAG, "waiting for writing ...");
-                Thread.sleep(TimeUnit.MILLISECONDS.convert(5L, TimeUnit.SECONDS));
+                dict.shutdownExecutorForTests();
+                while (!dict.isTerminatedForTests()) {
+                    Thread.sleep(WAIT_TERMINATING_IN_MILLISECONDS);
+                }
             } catch (InterruptedException e) {
                 Log.d(TAG, "InterruptedException: " + e);
             }
@@ -146,11 +153,11 @@
         final int numberOfWordsInsertedForEachLanguageSwitch = 100;
 
         final File dictFiles[] = new File[numberOfLanguages];
+        final String testFilenameSuffixes[] = new String[numberOfLanguages];
         try {
             final Random random = new Random(123456);
 
             // Create filename suffixes for this test.
-            String testFilenameSuffixes[] = new String[numberOfLanguages];
             for (int i = 0; i < numberOfLanguages; i++) {
                 testFilenameSuffixes[i] = "testSwitchingLanguages" + i;
                 final String fileName = UserHistoryPredictionDictionary.NAME + "." +
@@ -174,7 +181,15 @@
         } finally {
             try {
                 Log.d(TAG, "waiting for writing ...");
-                Thread.sleep(TimeUnit.MILLISECONDS.convert(5L, TimeUnit.SECONDS));
+                for (int i = 0; i < numberOfLanguages; i++) {
+                    final UserHistoryPredictionDictionary dict =
+                            PersonalizationHelper.getUserHistoryPredictionDictionary(getContext(),
+                                    testFilenameSuffixes[i], mPrefs);
+                    dict.shutdownExecutorForTests();
+                    while (!dict.isTerminatedForTests()) {
+                        Thread.sleep(WAIT_TERMINATING_IN_MILLISECONDS);
+                    }
+                }
             } catch (InterruptedException e) {
                 Log.d(TAG, "InterruptedException: " + e);
             }
diff --git a/tests/src/com/android/inputmethod/latin/utils/ResizableIntArrayTests.java b/tests/src/com/android/inputmethod/latin/utils/ResizableIntArrayTests.java
index cfff61e..cad80d5 100644
--- a/tests/src/com/android/inputmethod/latin/utils/ResizableIntArrayTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/ResizableIntArrayTests.java
@@ -340,4 +340,18 @@
                     expecteds[i + expectedPos], actuals[i + actualPos]);
         }
     }
+
+    public void testShift() {
+        final ResizableIntArray src = new ResizableIntArray(DEFAULT_CAPACITY);
+        final int limit = DEFAULT_CAPACITY * 10;
+        final int shiftAmount = 20;
+        for (int i = 0; i < limit; ++i) {
+            src.add(i, i);
+            assertEquals("length after add at " + i, i + 1, src.getLength());
+        }
+        src.shift(shiftAmount);
+        for (int i = 0; i < limit - shiftAmount; ++i) {
+            assertEquals("value at " + i, i + shiftAmount, src.get(i));
+        }
+    }
 }
diff --git a/tests/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtilsTests.java
index 72b9478..3eabe2b 100644
--- a/tests/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtilsTests.java
@@ -143,7 +143,7 @@
     }
 
     private void readDictFromFile(final File file, final OnAddWordListener listener) {
-        final Ver3DictDecoder dictDecoder = new Ver3DictDecoder(file, DictDecoder.USE_BYTEARRAY);
+        final DictDecoder dictDecoder = FormatSpec.getDictDecoder(file, DictDecoder.USE_BYTEARRAY);
         try {
             dictDecoder.openDictBuffer();
         } catch (FileNotFoundException e) {
diff --git a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtils.java b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtils.java
index 465b177..fa80385 100644
--- a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtils.java
+++ b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtils.java
@@ -18,9 +18,9 @@
 
 import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils;
 import com.android.inputmethod.latin.makedict.DictDecoder;
+import com.android.inputmethod.latin.makedict.FormatSpec;
 import com.android.inputmethod.latin.makedict.FusionDictionary;
 import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
-import com.android.inputmethod.latin.makedict.Ver3DictDecoder;
 
 import org.xml.sax.SAXException;
 
@@ -185,14 +185,14 @@
                     crash(filename, new RuntimeException(
                             filename + " does not seem to be a dictionary file"));
                 } else {
-                    final DictDecoder dictDecoder = new Ver3DictDecoder(decodedSpec.mFile,
+                    final DictDecoder dictDecoder = FormatSpec.getDictDecoder(file,
                             DictDecoder.USE_BYTEARRAY);
                     if (report) {
                         System.out.println("Format : Binary dictionary format");
                         System.out.println("Packaging : " + decodedSpec.describeChain());
                         System.out.println("Uncompressed size : " + decodedSpec.mFile.length());
                     }
-                    return dictDecoder.readDictionaryBinary(null);
+                    return dictDecoder.readDictionaryBinary(null, false /* deleteDictIfBroken */);
                 }
             }
         } catch (IOException e) {
diff --git a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/DictionaryMaker.java b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/DictionaryMaker.java
index 709b819..5302e97 100644
--- a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/DictionaryMaker.java
+++ b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/DictionaryMaker.java
@@ -267,8 +267,8 @@
     private static FusionDictionary readBinaryFile(final String binaryFilename)
             throws FileNotFoundException, IOException, UnsupportedFormatException {
         final File file = new File(binaryFilename);
-        final DictDecoder dictDecoder = new Ver3DictDecoder(file);
-        return dictDecoder.readDictionaryBinary(null);
+        final DictDecoder dictDecoder = FormatSpec.getDictDecoder(file);
+        return dictDecoder.readDictionaryBinary(null, false /* deleteDictIfBroken */);
     }
 
     /**
diff --git a/tools/dicttool/tests/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtilsTests.java b/tools/dicttool/tests/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtilsTests.java
index 47e2206..1eff497 100644
--- a/tools/dicttool/tests/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtilsTests.java
+++ b/tools/dicttool/tests/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtilsTests.java
@@ -18,12 +18,12 @@
 
 import com.android.inputmethod.latin.makedict.DictDecoder;
 import com.android.inputmethod.latin.makedict.DictEncoder;
+import com.android.inputmethod.latin.makedict.FormatSpec;
 import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
 import com.android.inputmethod.latin.makedict.FusionDictionary;
 import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions;
 import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray;
 import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
-import com.android.inputmethod.latin.makedict.Ver3DictDecoder;
 import com.android.inputmethod.latin.makedict.Ver3DictEncoder;
 
 import junit.framework.TestCase;
@@ -69,9 +69,10 @@
             assertEquals("Wrong decode spec", BinaryDictOffdeviceUtils.COMPRESSION, step);
         }
         assertEquals("Wrong decode spec", 3, decodeSpec.mDecoderSpec.size());
-        final DictDecoder dictDecoder = new Ver3DictDecoder(decodeSpec.mFile);
+        final DictDecoder dictDecoder = FormatSpec.getDictDecoder(decodeSpec.mFile);
         final FusionDictionary resultDict = dictDecoder.readDictionaryBinary(
-                null /* dict : an optional dictionary to add words to, or null */);
+                null /* dict : an optional dictionary to add words to, or null */,
+                false /* deleteDictIfBroken */);
         assertEquals("Dictionary can't be read back correctly",
                 FusionDictionary.findWordInTree(resultDict.mRootNodeArray, "foo").getFrequency(),
                 TEST_FREQ);
diff --git a/tools/make-keyboard-text/res/values-lo/donottranslate-more-keys.xml b/tools/make-keyboard-text/res/values-lo/donottranslate-more-keys.xml
new file mode 100644
index 0000000..1d8ffa8
--- /dev/null
+++ b/tools/make-keyboard-text/res/values-lo/donottranslate-more-keys.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 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.
+*/
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Label for "switch to alphabetic" key.
+         U+0E81: "ກ" LAO LETTER KO
+         U+0E82: "ຂ" LAO LETTER KHO SUNG
+         U+0E84: "ຄ" LAO LETTER KHO TAM -->
+    <string name="label_to_alpha_key">&#x0E81;&#x0E82;&#x0E84;</string>
+    <!-- U+20AD: "₭" KIP SIGN -->
+    <string name="keylabel_for_currency">&#x20AD;</string>
+</resources>
diff --git a/tools/make-keyboard-text/res/values-ne/donottranslate-more-keys.xml b/tools/make-keyboard-text/res/values-ne/donottranslate-more-keys.xml
new file mode 100644
index 0000000..9205e53
--- /dev/null
+++ b/tools/make-keyboard-text/res/values-ne/donottranslate-more-keys.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 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.
+*/
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Label for "switch to alphabetic" key.
+         U+0915: "क" DEVANAGARI LETTER KA
+         U+0916: "ख" DEVANAGARI LETTER KHA
+         U+0917: "ग" DEVANAGARI LETTER GA -->
+    <string name="label_to_alpha_key">&#x0915;&#x0916;&#x0917;</string>
+    <!-- U+0967: "१" DEVANAGARI DIGIT ONE -->
+    <string name="keylabel_for_symbols_1">&#x0967;</string>
+    <!-- U+0968: "२" DEVANAGARI DIGIT TWO -->
+    <string name="keylabel_for_symbols_2">&#x0968;</string>
+    <!-- U+0969: "३" DEVANAGARI DIGIT THREE -->
+    <string name="keylabel_for_symbols_3">&#x0969;</string>
+    <!-- U+096A: "४" DEVANAGARI DIGIT FOUR -->
+    <string name="keylabel_for_symbols_4">&#x096A;</string>
+    <!-- U+096B: "५" DEVANAGARI DIGIT FIVE -->
+    <string name="keylabel_for_symbols_5">&#x096B;</string>
+    <!-- U+096C: "६" DEVANAGARI DIGIT SIX -->
+    <string name="keylabel_for_symbols_6">&#x096C;</string>
+    <!-- U+096D: "७" DEVANAGARI DIGIT SEVEN -->
+    <string name="keylabel_for_symbols_7">&#x096D;</string>
+    <!-- U+096E: "८" DEVANAGARI DIGIT EIGHT -->
+    <string name="keylabel_for_symbols_8">&#x096E;</string>
+    <!-- U+096F: "९" DEVANAGARI DIGIT NINE -->
+    <string name="keylabel_for_symbols_9">&#x096F;</string>
+    <!-- U+0966: "०" DEVANAGARI DIGIT ZERO -->
+    <string name="keylabel_for_symbols_0">&#x0966;</string>
+    <!-- Label for "switch to symbols" key. -->
+    <string name="label_to_symbol_key">\?&#x0967;&#x0968;&#x0969;</string>
+    <!-- Label for "switch to symbols with microphone" key. This string shouldn't include the "mic"
+         part because it'll be appended by the code. -->
+    <string name="label_to_symbol_with_microphone_key">&#x0967;&#x0968;&#x0969;</string>
+    <string name="additional_more_keys_for_symbols_1">1</string>
+    <string name="additional_more_keys_for_symbols_2">2</string>
+    <string name="additional_more_keys_for_symbols_3">3</string>
+    <string name="additional_more_keys_for_symbols_4">4</string>
+    <string name="additional_more_keys_for_symbols_5">5</string>
+    <string name="additional_more_keys_for_symbols_6">6</string>
+    <string name="additional_more_keys_for_symbols_7">7</string>
+    <string name="additional_more_keys_for_symbols_8">8</string>
+    <string name="additional_more_keys_for_symbols_9">9</string>
+    <string name="additional_more_keys_for_symbols_0">0</string>
+    <!-- U+0930/U+0941/U+002E "रु." NEPALESE RUPEE SIGN -->
+    <string name="keylabel_for_currency">&#x0930;&#x0941;&#x002E;</string>
+</resources>
diff --git a/tools/make-keyboard-text/res/values/donottranslate-more-keys.xml b/tools/make-keyboard-text/res/values/donottranslate-more-keys.xml
index a4c2f12..cc09f7f 100644
--- a/tools/make-keyboard-text/res/values/donottranslate-more-keys.xml
+++ b/tools/make-keyboard-text/res/values/donottranslate-more-keys.xml
@@ -190,7 +190,7 @@
     <!-- Label for "switch to more symbol" modifier key.  Must be short to fit on key! -->
     <string name="label_to_more_symbol_key">= \\ &lt;</string>
     <!-- Label for "switch to more symbol" modifier key on tablets.  Must be short to fit on key! -->
-    <string name="label_to_more_symbol_for_tablet_key">~ [ {</string>
+    <string name="label_to_more_symbol_for_tablet_key">~ [ &lt;</string>
     <!-- Label for "Tab" key.  Must be short to fit on key! -->
     <string name="label_tab_key">Tab</string>
     <!-- Label for "switch to phone numeric" key.  Must be short to fit on key! -->