Merge "Make dictionary structure policy have updating methods."
diff --git a/java/res/drawable-hdpi/btn_center_default.9.png b/java/res/drawable-hdpi/btn_center_default.9.png
deleted file mode 100644
index 4f5f01c..0000000
--- a/java/res/drawable-hdpi/btn_center_default.9.png
+++ /dev/null
Binary files differ
diff --git a/java/res/drawable-hdpi/btn_center_pressed.9.png b/java/res/drawable-hdpi/btn_center_pressed.9.png
deleted file mode 100644
index 213b482..0000000
--- a/java/res/drawable-hdpi/btn_center_pressed.9.png
+++ /dev/null
Binary files differ
diff --git a/java/res/drawable-hdpi/btn_center_selected.9.png b/java/res/drawable-hdpi/btn_center_selected.9.png
deleted file mode 100644
index 213b482..0000000
--- a/java/res/drawable-hdpi/btn_center_selected.9.png
+++ /dev/null
Binary files differ
diff --git a/java/res/drawable-mdpi/btn_center_default.9.png b/java/res/drawable-mdpi/btn_center_default.9.png
deleted file mode 100644
index d5ec36b..0000000
--- a/java/res/drawable-mdpi/btn_center_default.9.png
+++ /dev/null
Binary files differ
diff --git a/java/res/drawable-mdpi/btn_center_pressed.9.png b/java/res/drawable-mdpi/btn_center_pressed.9.png
deleted file mode 100644
index 593a679..0000000
--- a/java/res/drawable-mdpi/btn_center_pressed.9.png
+++ /dev/null
Binary files differ
diff --git a/java/res/drawable-mdpi/btn_center_selected.9.png b/java/res/drawable-mdpi/btn_center_selected.9.png
deleted file mode 100644
index f1914a8..0000000
--- a/java/res/drawable-mdpi/btn_center_selected.9.png
+++ /dev/null
Binary files differ
diff --git a/java/res/drawable-xhdpi/btn_center_default.9.png b/java/res/drawable-xhdpi/btn_center_default.9.png
deleted file mode 100644
index e847425..0000000
--- a/java/res/drawable-xhdpi/btn_center_default.9.png
+++ /dev/null
Binary files differ
diff --git a/java/res/drawable-xhdpi/btn_center_pressed.9.png b/java/res/drawable-xhdpi/btn_center_pressed.9.png
deleted file mode 100644
index facfd43..0000000
--- a/java/res/drawable-xhdpi/btn_center_pressed.9.png
+++ /dev/null
Binary files differ
diff --git a/java/res/drawable-xhdpi/btn_center_selected.9.png b/java/res/drawable-xhdpi/btn_center_selected.9.png
deleted file mode 100644
index facfd43..0000000
--- a/java/res/drawable-xhdpi/btn_center_selected.9.png
+++ /dev/null
Binary files differ
diff --git a/java/res/drawable/btn_center.xml b/java/res/drawable/btn_center.xml
deleted file mode 100644
index 3ac2129..0000000
--- a/java/res/drawable/btn_center.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-**
-** Copyright 2011, 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.
-*/
--->
-
-<selector
-        xmlns:android="http://schemas.android.com/apk/res/android"
-        android:exitFadeDuration="@android:integer/config_mediumAnimTime">
-    <item
-        android:state_window_focused="false"
-        android:state_enabled="true"
-        android:drawable="@drawable/btn_center_default" />
-    <item
-        android:state_pressed="true"
-        android:drawable="@drawable/btn_center_pressed" />
-    <item
-        android:state_focused="true"
-        android:state_enabled="true"
-        android:drawable="@drawable/btn_center_selected" />
-    <item
-        android:state_enabled="true"
-        android:drawable="@drawable/btn_center_default" />
-    <item
-        android:drawable="@drawable/btn_center_default" />
-</selector>
diff --git a/java/res/drawable/btn_keyboard_key.xml b/java/res/drawable/btn_keyboard_key.xml
index 797bc10..112ac26 100644
--- a/java/res/drawable/btn_keyboard_key.xml
+++ b/java/res/drawable/btn_keyboard_key.xml
@@ -15,9 +15,7 @@
 -->
 
 <selector xmlns:android="http://schemas.android.com/apk/res/android">
-
     <!-- Toggle keys. Use checkable/checked state. -->
-
     <item android:state_checkable="true" android:state_checked="true"
           android:state_pressed="true"
           android:drawable="@drawable/btn_keyboard_key_pressed_on" />
@@ -28,11 +26,12 @@
     <item android:state_checkable="true"
           android:drawable="@drawable/btn_keyboard_key_normal_off" />
 
-    <!-- Normal keys -->
+    <!-- Empty background keys. -->
+    <item android:state_empty="true"
+          android:drawable="@drawable/transparent" />
 
+    <!-- Normal keys. -->
     <item android:state_pressed="true"
           android:drawable="@drawable/btn_keyboard_key_pressed" />
-    <item
-          android:drawable="@drawable/btn_keyboard_key_normal" />
-
+    <item android:drawable="@drawable/btn_keyboard_key_normal" />
 </selector>
diff --git a/java/res/drawable/btn_keyboard_key3.xml b/java/res/drawable/btn_keyboard_key3.xml
index dbe82d5..080b1f3 100644
--- a/java/res/drawable/btn_keyboard_key3.xml
+++ b/java/res/drawable/btn_keyboard_key3.xml
@@ -15,9 +15,7 @@
 -->
 
 <selector xmlns:android="http://schemas.android.com/apk/res/android">
-
     <!-- Toggle keys. Use checkable/checked state. -->
-
     <item android:state_checkable="true" android:state_checked="true"
           android:state_pressed="true"
           android:drawable="@drawable/btn_keyboard_key_pressed_on" />
@@ -28,8 +26,11 @@
     <item android:state_checkable="true"
           android:drawable="@drawable/btn_keyboard_key_fulltrans_pressed" />
 
-    <!-- Normal keys -->
+    <!-- Empty background keys. -->
+    <item android:state_empty="true"
+          android:drawable="@drawable/transparent" />
 
+    <!-- Normal keys. -->
     <item android:state_pressed="true"
           android:drawable="@drawable/btn_keyboard_key_fulltrans_normal" />
     <item android:drawable="@drawable/btn_keyboard_key_fulltrans_pressed" />
diff --git a/java/res/drawable/btn_keyboard_key_gingerbread.xml b/java/res/drawable/btn_keyboard_key_gingerbread.xml
index 5b4399e..3fc253e 100644
--- a/java/res/drawable/btn_keyboard_key_gingerbread.xml
+++ b/java/res/drawable/btn_keyboard_key_gingerbread.xml
@@ -15,23 +15,19 @@
 -->
 
 <selector xmlns:android="http://schemas.android.com/apk/res/android">
-
     <!-- Functional keys. -->
-
     <item android:state_single="true" android:state_pressed="true"
           android:drawable="@drawable/btn_keyboard_key_dark_pressed" />
     <item android:state_single="true"
           android:drawable="@drawable/btn_keyboard_key_dark_normal" />
 
     <!-- Action keys. -->
-
     <item android:state_active="true" android:state_pressed="true"
           android:drawable="@drawable/btn_keyboard_key_dark_pressed" />
     <item android:state_active="true"
           android:drawable="@drawable/btn_keyboard_key_dark_normal" />
 
     <!-- Toggle keys. Use checkable/checked state. -->
-
     <item android:state_checkable="true" android:state_checked="true" android:state_pressed="true"
           android:drawable="@drawable/btn_keyboard_key_dark_pressed_on" />
     <item android:state_checkable="true" android:state_pressed="true"
@@ -41,8 +37,11 @@
     <item android:state_checkable="true"
           android:drawable="@drawable/btn_keyboard_key_dark_normal_off" />
 
-    <!-- Normal keys. -->
+    <!-- Empty background keys. -->
+    <item android:state_empty="true"
+          android:drawable="@drawable/transparent" />
 
+    <!-- Normal keys. -->
     <item android:state_pressed="true"
           android:drawable="@drawable/btn_keyboard_key_light_pressed" />
     <item android:drawable="@drawable/btn_keyboard_key_light_normal" />
diff --git a/java/res/drawable/btn_keyboard_key_ics.xml b/java/res/drawable/btn_keyboard_key_ics.xml
index e893da1..0c86e16 100644
--- a/java/res/drawable/btn_keyboard_key_ics.xml
+++ b/java/res/drawable/btn_keyboard_key_ics.xml
@@ -15,23 +15,19 @@
 -->
 
 <selector xmlns:android="http://schemas.android.com/apk/res/android">
-
     <!-- Functional keys. -->
-
     <item android:state_single="true" android:state_pressed="true"
           android:drawable="@drawable/btn_keyboard_key_dark_pressed_holo" />
     <item android:state_single="true"
           android:drawable="@drawable/btn_keyboard_key_dark_normal_holo" />
 
     <!-- Action keys. -->
-
     <item android:state_active="true" android:state_pressed="true"
           android:drawable="@drawable/btn_keyboard_key_dark_pressed_holo" />
     <item android:state_active="true"
           android:drawable="@drawable/btn_keyboard_key_dark_active_holo" />
 
     <!-- Toggle keys. Use checkable/checked state. -->
-
     <item android:state_checkable="true" android:state_checked="true" android:state_pressed="true"
           android:drawable="@drawable/btn_keyboard_key_dark_pressed_on_holo" />
     <item android:state_checkable="true" android:state_pressed="true"
@@ -41,8 +37,11 @@
     <item android:state_checkable="true"
           android:drawable="@drawable/btn_keyboard_key_dark_normal_off_holo" />
 
-    <!-- Normal keys. -->
+    <!-- Empty background keys. -->
+    <item android:state_empty="true"
+          android:drawable="@drawable/transparent" />
 
+    <!-- Normal keys. -->
     <item android:state_pressed="true"
           android:drawable="@drawable/btn_keyboard_key_light_pressed_holo" />
     <item android:drawable="@drawable/btn_keyboard_key_light_normal_holo" />
diff --git a/java/res/drawable/btn_keyboard_key_stone.xml b/java/res/drawable/btn_keyboard_key_stone.xml
index 9bc3f18..70a2ad4 100644
--- a/java/res/drawable/btn_keyboard_key_stone.xml
+++ b/java/res/drawable/btn_keyboard_key_stone.xml
@@ -15,23 +15,19 @@
 -->
 
 <selector xmlns:android="http://schemas.android.com/apk/res/android">
-
     <!-- Functional keys. -->
-
     <item android:state_single="true" android:state_pressed="true"
           android:drawable="@drawable/btn_keyboard_key_fulltrans_pressed" />
     <item android:state_single="true"
           android:drawable="@drawable/btn_keyboard_key_normal_stone" />
 
     <!-- Action keys. -->
-
     <item android:state_active="true" android:state_pressed="true"
           android:drawable="@drawable/btn_keyboard_key_fulltrans_pressed" />
     <item android:state_active="true"
           android:drawable="@drawable/btn_keyboard_key_normal_stone" />
 
     <!-- Toggle keys. Use checkable/checked state. -->
-
     <item android:state_checkable="true" android:state_checked="true"
           android:state_pressed="true"
           android:drawable="@drawable/btn_keyboard_key_normal_on_stone" />
@@ -42,8 +38,11 @@
     <item android:state_checkable="true"
           android:drawable="@drawable/btn_keyboard_key_normal_off_stone" />
 
-    <!-- Normal keys. -->
+    <!-- Empty background keys. -->
+    <item android:state_empty="true"
+          android:drawable="@drawable/transparent" />
 
+    <!-- Normal keys. -->
     <item android:state_pressed="true"
           android:drawable="@drawable/btn_keyboard_key_fulltrans_pressed" />
     <item android:drawable="@drawable/btn_keyboard_key_normal_stone" />
diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml
index eef9116..5c59f5f 100644
--- a/java/res/values/attrs.xml
+++ b/java/res/values/attrs.xml
@@ -240,11 +240,12 @@
         <attr name="maxMoreKeysColumn" format="integer" />
         <attr name="backgroundType" format="enum">
             <!-- This should be aligned with Key.BACKGROUND_TYPE_* -->
-            <enum name="normal" value="0" />
-            <enum name="functional" value="1" />
-            <enum name="action" value="2" />
-            <enum name="stickyOff" value="3" />
-            <enum name="stickyOn" value="4" />
+            <enum name="empty" value="0" />
+            <enum name="normal" value="1" />
+            <enum name="functional" value="2" />
+            <enum name="action" value="3" />
+            <enum name="stickyOff" value="4" />
+            <enum name="stickyOn" value="5" />
         </attr>
         <!-- The key action flags. -->
         <attr name="keyActionFlags" format="integer">
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
index 6180528..16c79eb 100644
--- a/java/src/com/android/inputmethod/keyboard/Key.java
+++ b/java/src/com/android/inputmethod/keyboard/Key.java
@@ -122,11 +122,12 @@
 
     /** Background type that represents different key background visual than normal one. */
     public final int mBackgroundType;
-    public static final int BACKGROUND_TYPE_NORMAL = 0;
-    public static final int BACKGROUND_TYPE_FUNCTIONAL = 1;
-    public static final int BACKGROUND_TYPE_ACTION = 2;
-    public static final int BACKGROUND_TYPE_STICKY_OFF = 3;
-    public static final int BACKGROUND_TYPE_STICKY_ON = 4;
+    public static final int BACKGROUND_TYPE_EMPTY = 0;
+    public static final int BACKGROUND_TYPE_NORMAL = 1;
+    public static final int BACKGROUND_TYPE_FUNCTIONAL = 2;
+    public static final int BACKGROUND_TYPE_ACTION = 3;
+    public static final int BACKGROUND_TYPE_STICKY_OFF = 4;
+    public static final int BACKGROUND_TYPE_STICKY_ON = 5;
 
     private final int mActionFlags;
     private static final int ACTION_FLAGS_IS_REPEATABLE = 0x01;
@@ -175,7 +176,7 @@
     public Key(final KeyboardParams params, final MoreKeySpec moreKeySpec, final int x, final int y,
             final int width, final int height, final int labelFlags) {
         this(params, moreKeySpec.mLabel, null, moreKeySpec.mIconId, moreKeySpec.mCode,
-                moreKeySpec.mOutputText, x, y, width, height, labelFlags);
+                moreKeySpec.mOutputText, x, y, width, height, labelFlags, BACKGROUND_TYPE_NORMAL);
     }
 
     /**
@@ -183,12 +184,12 @@
      */
     public Key(final KeyboardParams params, final String label, final String hintLabel,
             final int iconId, final int code, final String outputText, final int x, final int y,
-            final int width, final int height, final int labelFlags) {
+            final int width, final int height, final int labelFlags, final int backgroundType) {
         mHeight = height - params.mVerticalGap;
         mWidth = width - params.mHorizontalGap;
         mHintLabel = hintLabel;
         mLabelFlags = labelFlags;
-        mBackgroundType = BACKGROUND_TYPE_NORMAL;
+        mBackgroundType = backgroundType;
         mActionFlags = 0;
         mMoreKeys = null;
         mMoreKeysColumnAndFlags = 0;
@@ -465,6 +466,7 @@
 
     private static String backgroundName(final int backgroundType) {
         switch (backgroundType) {
+        case BACKGROUND_TYPE_EMPTY: return "empty";
         case BACKGROUND_TYPE_NORMAL: return "normal";
         case BACKGROUND_TYPE_FUNCTIONAL: return "functional";
         case BACKGROUND_TYPE_ACTION: return "action";
@@ -788,6 +790,10 @@
         android.R.attr.state_pressed
     };
 
+    private final static int[] KEY_STATE_EMPTY = {
+        android.R.attr.state_empty
+    };
+
     // functional normal state (with properties)
     private static final int[] KEY_STATE_FUNCTIONAL_NORMAL = {
             android.R.attr.state_single
@@ -825,6 +831,8 @@
             return mPressed ? KEY_STATE_PRESSED_HIGHLIGHT_OFF : KEY_STATE_NORMAL_HIGHLIGHT_OFF;
         case BACKGROUND_TYPE_STICKY_ON:
             return mPressed ? KEY_STATE_PRESSED_HIGHLIGHT_ON : KEY_STATE_NORMAL_HIGHLIGHT_ON;
+        case BACKGROUND_TYPE_EMPTY:
+            return mPressed ? KEY_STATE_PRESSED : KEY_STATE_EMPTY;
         default: /* BACKGROUND_TYPE_NORMAL */
             return mPressed ? KEY_STATE_PRESSED : KEY_STATE_NORMAL;
         }
@@ -842,7 +850,7 @@
         protected Spacer(final KeyboardParams params, final int x, final int y, final int width,
                 final int height) {
             super(params, null, null, ICON_UNDEFINED, CODE_UNSPECIFIED,
-                    null, x, y, width, height, 0);
+                    null, x, y, width, height, 0, BACKGROUND_TYPE_EMPTY);
         }
     }
 }
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
index b266986..dc760e6 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
@@ -26,10 +26,10 @@
      *
      * @param primaryCode the unicode of the key being pressed. If the touch is not on a valid key,
      *            the value will be zero.
-     * @param isRepeatKey true if pressing has occurred while key repeat input.
+     * @param repeatCount how many times the key was repeated. Zero if it is the first press.
      * @param isSinglePointer true if pressing has occurred while no other key is being pressed.
      */
-    public void onPressKey(int primaryCode, boolean isRepeatKey, boolean isSinglePointer);
+    public void onPressKey(int primaryCode, int repeatCount, boolean isSinglePointer);
 
     /**
      * Called when the user releases a key. This is sent after the {@link #onCodeInput} is called.
@@ -103,7 +103,7 @@
 
     public static class Adapter implements KeyboardActionListener {
         @Override
-        public void onPressKey(int primaryCode, boolean isRepeatKey, boolean isSinglePointer) {}
+        public void onPressKey(int primaryCode, int repeatCount, boolean isSinglePointer) {}
         @Override
         public void onReleaseKey(int primaryCode, boolean withSliding) {}
         @Override
diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
index 526c2f1..f3d0ead 100644
--- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
@@ -217,7 +217,7 @@
                 startWhileTypingFadeinAnimation(keyboardView);
                 break;
             case MSG_REPEAT_KEY:
-                tracker.onKeyRepeat(msg.arg1);
+                tracker.onKeyRepeat(msg.arg1 /* code */, msg.arg2 /* repeatCount */);
                 break;
             case MSG_LONGPRESS_KEY:
                 keyboardView.onLongPress(tracker);
@@ -230,12 +230,14 @@
         }
 
         @Override
-        public void startKeyRepeatTimer(final PointerTracker tracker, final int delay) {
+        public void startKeyRepeatTimer(final PointerTracker tracker, final int repeatCount,
+                final int delay) {
             final Key key = tracker.getKey();
             if (key == null || delay == 0) {
                 return;
             }
-            sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, key.mCode, 0, tracker), delay);
+            sendMessageDelayed(
+                    obtainMessage(MSG_REPEAT_KEY, key.mCode, repeatCount, tracker), delay);
         }
 
         public void cancelKeyRepeatTimer() {
@@ -938,7 +940,7 @@
         if (key.hasNoPanelAutoMoreKey()) {
             final int moreKeyCode = key.mMoreKeys[0].mCode;
             tracker.onLongPressed();
-            listener.onPressKey(moreKeyCode, false /* isRepeatKey */, true /* isSinglePointer */);
+            listener.onPressKey(moreKeyCode, 0 /* repeatCount */, true /* isSinglePointer */);
             listener.onCodeInput(moreKeyCode,
                     Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
             listener.onReleaseKey(moreKeyCode, false /* withSliding */);
diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
index b66ee2a..5387ddb 100644
--- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java
+++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
@@ -64,7 +64,7 @@
 
         /**
          * Get KeyboardActionListener object that is used to register key code and so on.
-         * @return the KeyboardActionListner for this PointerTracker
+         * @return the KeyboardActionListner for this PointerTracke
          */
         public KeyboardActionListener getKeyboardActionListener();
 
@@ -94,7 +94,7 @@
     public interface TimerProxy {
         public void startTypingStateTimer(Key typedKey);
         public boolean isTypingState();
-        public void startKeyRepeatTimer(PointerTracker tracker, int delay);
+        public void startKeyRepeatTimer(PointerTracker tracker, int repeatCount, int delay);
         public void startLongPressTimer(PointerTracker tracker, int delay);
         public void cancelLongPressTimer();
         public void startDoubleTapShiftKeyTimer();
@@ -111,7 +111,7 @@
             @Override
             public boolean isTypingState() { return false; }
             @Override
-            public void startKeyRepeatTimer(PointerTracker tracker, int delay) {}
+            public void startKeyRepeatTimer(PointerTracker tracker, int repeatCount, int delay) {}
             @Override
             public void startLongPressTimer(PointerTracker tracker, int delay) {}
             @Override
@@ -490,7 +490,7 @@
 
     // Returns true if keyboard has been changed by this callback.
     private boolean callListenerOnPressAndCheckKeyboardLayoutChange(final Key key,
-            final boolean isRepeatKey) {
+            final int repeatCount) {
         // While gesture input is going on, this method should be a no-operation. But when gesture
         // input has been canceled, <code>sInGesture</code> and <code>mIsDetectingGesture</code>
         // are set to false. To keep this method is a no-operation,
@@ -504,13 +504,13 @@
                     KeyDetector.printableCode(key),
                     ignoreModifierKey ? " ignoreModifier" : "",
                     key.isEnabled() ? "" : " disabled",
-                    isRepeatKey ? " repeat" : ""));
+                    repeatCount > 0 ? " repeatCount=" + repeatCount : ""));
         }
         if (ignoreModifierKey) {
             return false;
         }
         if (key.isEnabled()) {
-            mListener.onPressKey(key.mCode, isRepeatKey, getActivePointerTrackerCount() == 1);
+            mListener.onPressKey(key.mCode, repeatCount, getActivePointerTrackerCount() == 1);
             final boolean keyboardLayoutHasBeenChanged = mKeyboardLayoutHasBeenChanged;
             mKeyboardLayoutHasBeenChanged = false;
             mTimerProxy.startTypingStateTimer(key);
@@ -967,7 +967,7 @@
             // This onPress call may have changed keyboard layout. Those cases are detected at
             // {@link #setKeyboard}. In those cases, we should update key according to the new
             // keyboard layout.
-            if (callListenerOnPressAndCheckKeyboardLayoutChange(key, false /* isRepeatKey */)) {
+            if (callListenerOnPressAndCheckKeyboardLayoutChange(key, 0 /* repeatCount */)) {
                 key = onDownKey(x, y, eventTime);
             }
 
@@ -1057,7 +1057,7 @@
         // at {@link #setKeyboard}. In those cases, we should update key according
         // to the new keyboard layout.
         Key key = newKey;
-        if (callListenerOnPressAndCheckKeyboardLayoutChange(key, false /* isRepeatKey */)) {
+        if (callListenerOnPressAndCheckKeyboardLayoutChange(key, 0 /* repeatCount */)) {
             key = onMoveKey(x, y);
         }
         onMoveToNewKey(key, x, y);
@@ -1413,16 +1413,18 @@
         // Don't start key repeat when we are in sliding input mode.
         if (mIsInSlidingKeyInput) return;
         detectAndSendKey(key, key.mX, key.mY, SystemClock.uptimeMillis());
-        mTimerProxy.startKeyRepeatTimer(this, sParams.mKeyRepeatStartTimeout);
+        final int startRepeatCount = 1;
+        mTimerProxy.startKeyRepeatTimer(this, startRepeatCount, sParams.mKeyRepeatStartTimeout);
     }
 
-    public void onKeyRepeat(final int code) {
+    public void onKeyRepeat(final int code, final int repeatCount) {
         final Key key = getKey();
         if (key == null || key.mCode != code) {
             return;
         }
-        mTimerProxy.startKeyRepeatTimer(this, sParams.mKeyRepeatInterval);
-        callListenerOnPressAndCheckKeyboardLayoutChange(key, true /* isRepeatKey */);
+        final int nextRepeatCount = repeatCount + 1;
+        mTimerProxy.startKeyRepeatTimer(this, nextRepeatCount, sParams.mKeyRepeatInterval);
+        callListenerOnPressAndCheckKeyboardLayoutChange(key, repeatCount);
         callListenerOnCodeInput(key, code, mKeyX, mKeyY, SystemClock.uptimeMillis());
     }
 
diff --git a/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java b/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java
index 42c5794..54bc295 100644
--- a/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java
+++ b/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java
@@ -57,10 +57,10 @@
         mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
     }
 
-    public void hapticAndAudioFeedback(final int primaryCode,
+    public void performHapticAndAudioFeedback(final int code,
             final View viewToPerformHapticFeedbackOn) {
-        vibrateInternal(viewToPerformHapticFeedbackOn);
-        playKeyClick(primaryCode);
+        performHapticFeedback(viewToPerformHapticFeedbackOn);
+        performAudioFeedback(code);
     }
 
     public boolean hasVibrator() {
@@ -81,14 +81,14 @@
         return mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_NORMAL;
     }
 
-    private void playKeyClick(final int primaryCode) {
+    public void performAudioFeedback(final int code) {
         // if mAudioManager is null, we can't play a sound anyway, so return
         if (mAudioManager == null) {
             return;
         }
         if (mSoundOn) {
             final int sound;
-            switch (primaryCode) {
+            switch (code) {
             case Constants.CODE_DELETE:
                 sound = AudioManager.FX_KEYPRESS_DELETE;
                 break;
@@ -106,7 +106,7 @@
         }
     }
 
-    private void vibrateInternal(final View viewToPerformHapticFeedbackOn) {
+    public void performHapticFeedback(final View viewToPerformHapticFeedbackOn) {
         if (!mSettingsValues.mVibrateOn) {
             return;
         }
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index b14ee31..ffe3171 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -122,6 +122,8 @@
 
     private static final int PENDING_IMS_CALLBACK_DURATION = 800;
 
+    private static final int PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT = 2;
+
     /**
      * The name of the scheme used by the Package Manager to warn of a new package installation,
      * replacement or removal.
@@ -1355,10 +1357,6 @@
         }
     }
 
-    private static boolean isAlphabet(final int code) {
-        return Character.isLetter(code);
-    }
-
     private void onSettingsKeyPressed() {
         if (isShowingOptionDialog()) return;
         showSubtypeSelectorAndSettings();
@@ -1466,7 +1464,7 @@
             break;
         case Constants.CODE_SHIFT:
             // Note: Calling back to the keyboard on Shift key is handled in
-            // {@link #onPressKey(int,boolean)} and {@link #onReleaseKey(int,boolean)}.
+            // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
             final Keyboard currentKeyboard = switcher.getKeyboard();
             if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) {
                 // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for
@@ -1480,7 +1478,7 @@
             break;
         case Constants.CODE_SWITCH_ALPHA_SYMBOL:
             // Note: Calling back to the keyboard on symbol key is handled in
-            // {@link #onPressKey(int,boolean)} and {@link #onReleaseKey(int,boolean)}.
+            // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
             break;
         case Constants.CODE_SETTINGS:
             onSettingsKeyPressed();
@@ -1848,23 +1846,23 @@
             // When we exit this if-clause, mWordComposer.isComposingWord() will return false.
         }
         if (mWordComposer.isComposingWord()) {
-            final int length = mWordComposer.size();
-            if (length > 0) {
-                if (mWordComposer.isBatchMode()) {
-                    if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-                        final String word = mWordComposer.getTypedWord();
-                        ResearchLogger.latinIME_handleBackspace_batch(word, 1);
-                    }
-                    final String rejectedSuggestion = mWordComposer.getTypedWord();
-                    mWordComposer.reset();
-                    mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion);
-                } else {
-                    mWordComposer.deleteLast();
+            if (mWordComposer.isBatchMode()) {
+                if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+                    final String word = mWordComposer.getTypedWord();
+                    ResearchLogger.latinIME_handleBackspace_batch(word, 1);
                 }
-                mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
-                mHandler.postUpdateSuggestionStrip();
+                final String rejectedSuggestion = mWordComposer.getTypedWord();
+                mWordComposer.reset();
+                mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion);
             } else {
-                mConnection.deleteSurroundingText(1, 0);
+                mWordComposer.deleteLast();
+            }
+            mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
+            mHandler.postUpdateSuggestionStrip();
+            if (!mWordComposer.isComposingWord()) {
+                // If we just removed the last character, auto-caps mode may have changed so we
+                // need to re-evaluate.
+                mKeyboardSwitcher.updateShiftState();
             }
         } else {
             final SettingsValues currentSettings = mSettings.getCurrent();
@@ -1879,8 +1877,7 @@
                 // Cancel multi-character input: remove the text we just entered.
                 // This is triggered on backspace after a key that inputs multiple characters,
                 // like the smiley key or the .com key.
-                final int length = mEnteredText.length();
-                mConnection.deleteSurroundingText(length, 0);
+                mConnection.deleteSurroundingText(mEnteredText.length(), 0);
                 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
                     ResearchLogger.latinIME_handleBackspace_cancelTextInput(mEnteredText);
                 }
@@ -1926,6 +1923,8 @@
                     // This should never happen.
                     Log.e(TAG, "Backspace when we don't know the selection position");
                 }
+                final int lengthToDelete = Character.isSupplementaryCodePoint(
+                        mConnection.getCodePointBeforeCursor()) ? 2 : 1;
                 if (mAppWorkAroundsUtils.isBeforeJellyBean()) {
                     // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
                     // a hardware keyboard event on pressing enter or delete. This is bad for many
@@ -1933,15 +1932,18 @@
                     // relying on this behavior so we continue to support it for older apps.
                     sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_DEL);
                 } else {
-                    mConnection.deleteSurroundingText(1, 0);
+                    mConnection.deleteSurroundingText(lengthToDelete, 0);
                 }
                 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-                    ResearchLogger.latinIME_handleBackspace(1, true /* shouldUncommitLogUnit */);
+                    ResearchLogger.latinIME_handleBackspace(lengthToDelete,
+                            true /* shouldUncommitLogUnit */);
                 }
                 if (mDeleteCount > DELETE_ACCELERATE_AT) {
-                    mConnection.deleteSurroundingText(1, 0);
+                    final int lengthToDeleteAgain = Character.isSupplementaryCodePoint(
+                            mConnection.getCodePointBeforeCursor()) ? 2 : 1;
+                    mConnection.deleteSurroundingText(lengthToDeleteAgain, 0);
                     if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-                        ResearchLogger.latinIME_handleBackspace(1,
+                        ResearchLogger.latinIME_handleBackspace(lengthToDeleteAgain,
                                 true /* shouldUncommitLogUnit */);
                     }
                 }
@@ -1949,6 +1951,8 @@
             if (currentSettings.isSuggestionsRequested(mDisplayOrientation)) {
                 restartSuggestionsOnWordBeforeCursorIfAtEndOfWord();
             }
+            // We just removed a character. We need to update the auto-caps state.
+            mKeyboardSwitcher.updateShiftState();
         }
     }
 
@@ -1995,8 +1999,7 @@
         // NOTE: isCursorTouchingWord() is a blocking IPC call, so it often takes several
         // dozen milliseconds. Avoid calling it as much as possible, since we are on the UI
         // thread here.
-        if (!isComposingWord && (isAlphabet(primaryCode)
-                || currentSettings.isWordConnector(primaryCode))
+        if (!isComposingWord && currentSettings.isWordCodePoint(primaryCode)
                 && currentSettings.isSuggestionsRequested(mDisplayOrientation) &&
                 !mConnection.isCursorTouchingWord(currentSettings)) {
             // Reset entirely the composing state anyway, then start composing a new word unless
@@ -2558,8 +2561,8 @@
             }
         }
         mWordComposer.setComposingWord(typedWord, mKeyboardSwitcher.getKeyboard());
-        // TODO: this is in chars but the callee expects code points!
-        mWordComposer.setCursorPositionWithinWord(numberOfCharsInWordBeforeCursor);
+        mWordComposer.setCursorPositionWithinWord(
+                typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor));
         mConnection.setComposingRegion(
                 mLastSelectionStart - numberOfCharsInWordBeforeCursor,
                 mLastSelectionEnd + range.getNumberOfCharsInWordAfterCursor());
@@ -2697,30 +2700,43 @@
         }
     }
 
-    private void hapticAndAudioFeedback(final int code, final boolean isRepeatKey) {
+    private void hapticAndAudioFeedback(final int code, final int repeatCount) {
         final MainKeyboardView keyboardView = mKeyboardSwitcher.getMainKeyboardView();
         if (keyboardView != null && keyboardView.isInSlidingKeyInput()) {
             // No need to feedback while sliding input.
             return;
         }
-        if (isRepeatKey) {
-            // No need to feedback when repeating key.
-            return;
+        if (repeatCount > 0) {
+            if (code == Constants.CODE_DELETE && !mConnection.canDeleteCharacters()) {
+                // No need to feedback when repeat delete key will have no effect.
+                return;
+            }
+            // TODO: Use event time that the last feedback has been generated instead of relying on
+            // a repeat count to thin out feedback.
+            if (repeatCount % PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT == 0) {
+                return;
+            }
         }
-        AudioAndHapticFeedbackManager.getInstance().hapticAndAudioFeedback(code, keyboardView);
+        final AudioAndHapticFeedbackManager feedbackManager =
+                AudioAndHapticFeedbackManager.getInstance();
+        if (repeatCount == 0) {
+            // TODO: Reconsider how to perform haptic feedback when repeating key.
+            feedbackManager.performHapticFeedback(keyboardView);
+        }
+        feedbackManager.performAudioFeedback(code);
     }
 
     // Callback of the {@link KeyboardActionListener}. This is called when a key is depressed;
     // release matching call is {@link #onReleaseKey(int,boolean)} below.
     @Override
-    public void onPressKey(final int primaryCode, final boolean isRepeatKey,
+    public void onPressKey(final int primaryCode, final int repeatCount,
             final boolean isSinglePointer) {
         mKeyboardSwitcher.onPressKey(primaryCode, isSinglePointer);
-        hapticAndAudioFeedback(primaryCode, isRepeatKey);
+        hapticAndAudioFeedback(primaryCode, repeatCount);
     }
 
     // Callback of the {@link KeyboardActionListener}. This is called when a key is released;
-    // press matching call is {@link #onPressKey(int,boolean,boolean)} above.
+    // press matching call is {@link #onPressKey(int,int,boolean)} above.
     @Override
     public void onReleaseKey(final int primaryCode, final boolean withSliding) {
         mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding);
@@ -2736,17 +2752,6 @@
                 break;
             }
         }
-
-        if (Constants.CODE_DELETE == primaryCode) {
-            // This is a stopgap solution to avoid leaving a high surrogate alone in a text view.
-            // In the future, we need to deprecate deteleSurroundingText() and have a surrogate
-            // pair-friendly way of deleting characters in InputConnection.
-            // TODO: use getCodePointBeforeCursor instead to improve performance
-            final CharSequence lastChar = mConnection.getTextBeforeCursor(1, 0);
-            if (!TextUtils.isEmpty(lastChar) && Character.isHighSurrogate(lastChar.charAt(0))) {
-                mConnection.deleteSurroundingText(1, 0);
-            }
-        }
     }
 
     // Hooks for hardware keyboard
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index c2fdcb5..55b70c6 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -107,7 +107,7 @@
     }
 
     private void addOrReplaceDictionaryInternal(final String key, final Dictionary dict) {
-        if (mOnlyDictionarySetForDebug != null && mOnlyDictionarySetForDebug.contains(key)) {
+        if (mOnlyDictionarySetForDebug != null && !mOnlyDictionarySetForDebug.contains(key)) {
             Log.w(TAG, "Ignore add " + key + " dictionary for debug.");
             return;
         }
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
index c7b063d..2e6c4b2 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
@@ -32,16 +32,12 @@
 import java.io.OutputStream;
 import java.nio.channels.FileChannel;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.Stack;
 
 public final class BinaryDictIOUtils {
     private static final boolean DBG = false;
-    private static final int MSB24 = 0x800000;
-    private static final int SINT24_MAX = 0x7FFFFF;
-    private static final int MAX_JUMPS = 10000;
 
     private BinaryDictIOUtils() {
         // This utility class is not publicly instantiable.
@@ -91,7 +87,7 @@
 
             if (p.mNumOfCharGroup == Position.NOT_READ_GROUPCOUNT) {
                 p.mNumOfCharGroup = BinaryDictInputOutput.readCharGroupCount(buffer);
-                p.mAddress += BinaryDictInputOutput.getGroupCountSize(p.mNumOfCharGroup);
+                p.mAddress += getGroupCountSize(p.mNumOfCharGroup);
                 p.mPosition = 0;
             }
             if (p.mNumOfCharGroup == 0) {
@@ -105,9 +101,9 @@
             }
             p.mPosition++;
 
-            final boolean isMovedGroup = BinaryDictInputOutput.isMovedGroup(info.mFlags,
+            final boolean isMovedGroup = isMovedGroup(info.mFlags,
                     formatOptions);
-            final boolean isDeletedGroup = BinaryDictInputOutput.isDeletedGroup(info.mFlags,
+            final boolean isDeletedGroup = isDeletedGroup(info.mFlags,
                     formatOptions);
             if (!isMovedGroup && !isDeletedGroup
                     && info.mFrequency != FusionDictionary.CharGroup.NOT_A_TERMINAL) {// found word
@@ -134,7 +130,7 @@
                 p.mAddress = buffer.position();
             }
 
-            if (!isMovedGroup && BinaryDictInputOutput.hasChildrenAddress(info.mChildrenAddress)) {
+            if (!isMovedGroup && hasChildrenAddress(info.mChildrenAddress)) {
                 Position childrenPos = new Position(info.mChildrenAddress + headerSize, index);
                 stack.push(childrenPos);
             }
@@ -191,12 +187,10 @@
                     final int charGroupPos = buffer.position();
                     final CharGroupInfo currentInfo = BinaryDictInputOutput.readCharGroup(buffer,
                             buffer.position(), header.mFormatOptions);
-                    final boolean isMovedGroup =
-                            BinaryDictInputOutput.isMovedGroup(currentInfo.mFlags,
-                                    header.mFormatOptions);
-                    final boolean isDeletedGroup =
-                            BinaryDictInputOutput.isDeletedGroup(currentInfo.mFlags,
-                                    header.mFormatOptions);
+                    final boolean isMovedGroup = isMovedGroup(currentInfo.mFlags,
+                            header.mFormatOptions);
+                    final boolean isDeletedGroup = isDeletedGroup(currentInfo.mFlags,
+                            header.mFormatOptions);
                     if (isMovedGroup) continue;
                     boolean same = true;
                     for (int p = 0, j = word.offsetByCodePoints(0, wordPos);
@@ -248,36 +242,10 @@
         return FormatSpec.NOT_VALID_WORD;
     }
 
-    private static int markAsDeleted(final int flags) {
-        return (flags & (~FormatSpec.MASK_GROUP_ADDRESS_TYPE)) | FormatSpec.FLAG_IS_DELETED;
-    }
-
-    /**
-     * Delete the word from the binary file.
-     *
-     * @param buffer the buffer to write.
-     * @param word the word we delete
-     * @throws IOException
-     * @throws UnsupportedFormatException
-     */
-    @UsedForTesting
-    public static void deleteWord(final FusionDictionaryBufferInterface buffer,
-            final String word) throws IOException, UnsupportedFormatException {
-        buffer.position(0);
-        final FileHeader header = BinaryDictInputOutput.readHeader(buffer);
-        final int wordPosition = getTerminalPosition(buffer, word);
-        if (wordPosition == FormatSpec.NOT_VALID_WORD) return;
-
-        buffer.position(wordPosition);
-        final int flags = buffer.readUnsignedByte();
-        buffer.position(wordPosition);
-        buffer.put((byte)markAsDeleted(flags));
-    }
-
     /**
      * @return the size written, in bytes. Always 3 bytes.
      */
-    private static int writeSInt24ToBuffer(final FusionDictionaryBufferInterface buffer,
+    static int writeSInt24ToBuffer(final FusionDictionaryBufferInterface buffer,
             final int value) {
         final int absValue = Math.abs(value);
         buffer.put((byte)(((value < 0 ? 0x80 : 0) | (absValue >> 16)) & 0xFF));
@@ -289,7 +257,7 @@
     /**
      * @return the size written, in bytes. Always 3 bytes.
      */
-    private static int writeSInt24ToStream(final OutputStream destination, final int value)
+    static int writeSInt24ToStream(final OutputStream destination, final int value)
             throws IOException {
         final int absValue = Math.abs(value);
         destination.write((byte)(((value < 0 ? 0x80 : 0) | (absValue >> 16)) & 0xFF));
@@ -320,39 +288,7 @@
         return BinaryDictInputOutput.getByteSize(value);
     }
 
-    /**
-     * Update a parent address in a CharGroup that is referred to by groupOriginAddress.
-     *
-     * @param buffer the buffer to write.
-     * @param groupOriginAddress the address of the group.
-     * @param newParentAddress the absolute address of the parent.
-     * @param formatOptions file format options.
-     */
-    public static void updateParentAddress(final FusionDictionaryBufferInterface buffer,
-            final int groupOriginAddress, final int newParentAddress,
-            final FormatOptions formatOptions) {
-        final int originalPosition = buffer.position();
-        buffer.position(groupOriginAddress);
-        if (!formatOptions.mSupportsDynamicUpdate) {
-            throw new RuntimeException("this file format does not support parent addresses");
-        }
-        final int flags = buffer.readUnsignedByte();
-        if (BinaryDictInputOutput.isMovedGroup(flags, formatOptions)) {
-            // if the group is moved, the parent address is stored in the destination group.
-            // We are guaranteed to process the destination group later, so there is no need to
-            // update anything here.
-            buffer.position(originalPosition);
-            return;
-        }
-        if (DBG) {
-            MakedictLog.d("update parent address flags=" + flags + ", " + groupOriginAddress);
-        }
-        final int parentOffset = newParentAddress - groupOriginAddress;
-        writeSInt24ToBuffer(buffer, parentOffset);
-        buffer.position(originalPosition);
-    }
-
-    private static void skipCharGroup(final FusionDictionaryBufferInterface buffer,
+    static void skipCharGroup(final FusionDictionaryBufferInterface buffer,
             final FormatOptions formatOptions) {
         final int flags = buffer.readUnsignedByte();
         BinaryDictInputOutput.readParentAddress(buffer, formatOptions);
@@ -387,33 +323,7 @@
         }
     }
 
-    /**
-     * Update parent addresses in a Node that is referred to by nodeOriginAddress.
-     *
-     * @param buffer the buffer to be modified.
-     * @param nodeOriginAddress the address of a modified Node.
-     * @param newParentAddress the address to be written.
-     * @param formatOptions file format options.
-     */
-    public static void updateParentAddresses(final FusionDictionaryBufferInterface buffer,
-            final int nodeOriginAddress, final int newParentAddress,
-            final FormatOptions formatOptions) {
-        final int originalPosition = buffer.position();
-        buffer.position(nodeOriginAddress);
-        do {
-            final int count = BinaryDictInputOutput.readCharGroupCount(buffer);
-            for (int i = 0; i < count; ++i) {
-                updateParentAddress(buffer, buffer.position(), newParentAddress, formatOptions);
-                skipCharGroup(buffer, formatOptions);
-            }
-            final int forwardLinkAddress = buffer.readUnsignedInt24();
-            buffer.position(forwardLinkAddress);
-        } while (formatOptions.mSupportsDynamicUpdate
-                && buffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS);
-        buffer.position(originalPosition);
-    }
-
-    private static void skipString(final FusionDictionaryBufferInterface buffer,
+    static void skipString(final FusionDictionaryBufferInterface buffer,
             final boolean hasMultipleChars) {
         if (hasMultipleChars) {
             int character = CharEncoding.readChar(buffer);
@@ -455,29 +365,6 @@
     }
 
     /**
-     * Update a children address in a CharGroup that is addressed by groupOriginAddress.
-     *
-     * @param buffer the buffer to write.
-     * @param groupOriginAddress the address of the group.
-     * @param newChildrenAddress the absolute address of the child.
-     * @param formatOptions file format options.
-     */
-    public static void updateChildrenAddress(final FusionDictionaryBufferInterface buffer,
-            final int groupOriginAddress, final int newChildrenAddress,
-            final FormatOptions formatOptions) {
-        final int originalPosition = buffer.position();
-        buffer.position(groupOriginAddress);
-        final int flags = buffer.readUnsignedByte();
-        final int parentAddress = BinaryDictInputOutput.readParentAddress(buffer, formatOptions);
-        skipString(buffer, (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS) != 0);
-        if ((flags & FormatSpec.FLAG_IS_TERMINAL) != 0) buffer.readUnsignedByte();
-        final int childrenOffset = newChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS
-                ? FormatSpec.NO_CHILDREN_ADDRESS : newChildrenAddress - buffer.position();
-        writeSInt24ToBuffer(buffer, childrenOffset);
-        buffer.position(originalPosition);
-    }
-
-    /**
      * Write a char group to an output stream.
      * A char group is an in-memory representation of a node in trie.
      * A char group info is an on-disk representation of a node.
@@ -569,52 +456,10 @@
         return size;
     }
 
-    @SuppressWarnings("unused")
-    private static void updateForwardLink(final FusionDictionaryBufferInterface buffer,
-            final int nodeOriginAddress, final int newNodeAddress,
-            final FormatOptions formatOptions) {
-        buffer.position(nodeOriginAddress);
-        int jumpCount = 0;
-        while (jumpCount++ < MAX_JUMPS) {
-            final int count = BinaryDictInputOutput.readCharGroupCount(buffer);
-            for (int i = 0; i < count; ++i) skipCharGroup(buffer, formatOptions);
-            final int forwardLinkAddress = buffer.readUnsignedInt24();
-            if (forwardLinkAddress == FormatSpec.NO_FORWARD_LINK_ADDRESS) {
-                buffer.position(buffer.position() - FormatSpec.FORWARD_LINK_ADDRESS_SIZE);
-                writeSInt24ToBuffer(buffer, newNodeAddress);
-                return;
-            }
-            buffer.position(forwardLinkAddress);
-        }
-        if (DBG && jumpCount >= MAX_JUMPS) {
-            throw new RuntimeException("too many jumps, probably a bug.");
-        }
-    }
-
-    /**
-     * Helper method to move a char group to the tail of the file.
-     */
-    private static int moveCharGroup(final OutputStream destination,
-            final FusionDictionaryBufferInterface buffer, final CharGroupInfo info,
-            final int nodeOriginAddress, final int oldGroupAddress,
-            final FormatOptions formatOptions) throws IOException {
-        updateParentAddress(buffer, oldGroupAddress, buffer.limit() + 1, formatOptions);
-        buffer.position(oldGroupAddress);
-        final int currentFlags = buffer.readUnsignedByte();
-        buffer.position(oldGroupAddress);
-        buffer.put((byte)(FormatSpec.FLAG_IS_MOVED | (currentFlags
-                & (~FormatSpec.MASK_MOVE_AND_DELETE_FLAG))));
-        int size = FormatSpec.GROUP_FLAGS_SIZE;
-        updateForwardLink(buffer, nodeOriginAddress, buffer.limit(), formatOptions);
-        size += writeNode(destination, new CharGroupInfo[] { info });
-        return size;
-    }
-
     /**
      * Compute the size of the char group.
      */
-    private static int computeGroupSize(final CharGroupInfo info,
-            final FormatOptions formatOptions) {
+    static int computeGroupSize(final CharGroupInfo info, final FormatOptions formatOptions) {
         int size = FormatSpec.GROUP_FLAGS_SIZE + FormatSpec.PARENT_ADDRESS_SIZE
                 + BinaryDictInputOutput.getGroupCharactersSize(info.mCharacters)
                 + BinaryDictInputOutput.getChildrenAddressSize(info.mFlags, formatOptions);
@@ -641,10 +486,10 @@
      * @return the size written, in bytes.
      * @throws IOException
      */
-    private static int writeNode(final OutputStream destination, final CharGroupInfo[] infos)
+    static int writeNode(final OutputStream destination, final CharGroupInfo[] infos)
             throws IOException {
-        int size = BinaryDictInputOutput.getGroupCountSize(infos.length);
-        switch (BinaryDictInputOutput.getGroupCountSize(infos.length)) {
+        int size = getGroupCountSize(infos.length);
+        switch (getGroupCountSize(infos.length)) {
             case 1:
                 destination.write((byte)infos.length);
                 break;
@@ -661,306 +506,6 @@
     }
 
     /**
-     * Move a group that is referred to by oldGroupOrigin to the tail of the file.
-     * And set the children address to the byte after the group.
-     *
-     * @param nodeOrigin the address of the tail of the file.
-     * @param characters
-     * @param length
-     * @param flags
-     * @param frequency
-     * @param parentAddress
-     * @param shortcutTargets
-     * @param bigrams
-     * @param destination the stream representing the tail of the file.
-     * @param buffer the buffer representing the (constant-size) body of the file.
-     * @param oldNodeOrigin
-     * @param oldGroupOrigin
-     * @param formatOptions
-     * @return the size written, in bytes.
-     * @throws IOException
-     */
-    private static int moveGroup(final int nodeOrigin, final int[] characters, final int length,
-            final int flags, final int frequency, final int parentAddress,
-            final ArrayList<WeightedString> shortcutTargets,
-            final ArrayList<PendingAttribute> bigrams, final OutputStream destination,
-            final FusionDictionaryBufferInterface buffer, final int oldNodeOrigin,
-            final int oldGroupOrigin, final FormatOptions formatOptions) throws IOException {
-        int size = 0;
-        final int newGroupOrigin = nodeOrigin + 1;
-        final int[] writtenCharacters = Arrays.copyOfRange(characters, 0, length);
-        final CharGroupInfo tmpInfo = new CharGroupInfo(newGroupOrigin, -1 /* endAddress */,
-                flags, writtenCharacters, frequency, parentAddress, FormatSpec.NO_CHILDREN_ADDRESS,
-                shortcutTargets, bigrams);
-        size = computeGroupSize(tmpInfo, formatOptions);
-        final CharGroupInfo newInfo = new CharGroupInfo(newGroupOrigin, newGroupOrigin + size,
-                flags, writtenCharacters, frequency, parentAddress,
-                nodeOrigin + 1 + size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE, shortcutTargets,
-                bigrams);
-        moveCharGroup(destination, buffer, newInfo, oldNodeOrigin, oldGroupOrigin, formatOptions);
-        return 1 + size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE;
-    }
-
-    /**
-     * Insert a word into a binary dictionary.
-     *
-     * @param buffer
-     * @param destination
-     * @param word
-     * @param frequency
-     * @param bigramStrings
-     * @param shortcuts
-     * @throws IOException
-     * @throws UnsupportedFormatException
-     */
-    // TODO: Support batch insertion.
-    // TODO: Remove @UsedForTesting once UserHistoryDictionary is implemented by BinaryDictionary.
-    @UsedForTesting
-    public static void insertWord(final FusionDictionaryBufferInterface buffer,
-            final OutputStream destination, final String word, final int frequency,
-            final ArrayList<WeightedString> bigramStrings,
-            final ArrayList<WeightedString> shortcuts, final boolean isNotAWord,
-            final boolean isBlackListEntry)
-                    throws IOException, UnsupportedFormatException {
-        final ArrayList<PendingAttribute> bigrams = new ArrayList<PendingAttribute>();
-        if (bigramStrings != null) {
-            for (final WeightedString bigram : bigramStrings) {
-                int position = getTerminalPosition(buffer, bigram.mWord);
-                if (position == FormatSpec.NOT_VALID_WORD) {
-                    // TODO: figure out what is the correct thing to do here.
-                } else {
-                    bigrams.add(new PendingAttribute(bigram.mFrequency, position));
-                }
-            }
-        }
-
-        final boolean isTerminal = true;
-        final boolean hasBigrams = !bigrams.isEmpty();
-        final boolean hasShortcuts = shortcuts != null && !shortcuts.isEmpty();
-
-        // find the insert position of the word.
-        if (buffer.position() != 0) buffer.position(0);
-        final FileHeader header = BinaryDictInputOutput.readHeader(buffer);
-
-        int wordPos = 0, address = buffer.position(), nodeOriginAddress = buffer.position();
-        final int[] codePoints = FusionDictionary.getCodePoints(word);
-        final int wordLen = codePoints.length;
-
-        for (int depth = 0; depth < Constants.DICTIONARY_MAX_WORD_LENGTH; ++depth) {
-            if (wordPos >= wordLen) break;
-            nodeOriginAddress = buffer.position();
-            int nodeParentAddress = -1;
-            final int charGroupCount = BinaryDictInputOutput.readCharGroupCount(buffer);
-            boolean foundNextGroup = false;
-
-            for (int i = 0; i < charGroupCount; ++i) {
-                address = buffer.position();
-                final CharGroupInfo currentInfo = BinaryDictInputOutput.readCharGroup(buffer,
-                        buffer.position(), header.mFormatOptions);
-                final boolean isMovedGroup = BinaryDictInputOutput.isMovedGroup(currentInfo.mFlags,
-                        header.mFormatOptions);
-                if (isMovedGroup) continue;
-                nodeParentAddress = (currentInfo.mParentAddress == FormatSpec.NO_PARENT_ADDRESS)
-                        ? FormatSpec.NO_PARENT_ADDRESS : currentInfo.mParentAddress + address;
-                boolean matched = true;
-                for (int p = 0; p < currentInfo.mCharacters.length; ++p) {
-                    if (wordPos + p >= wordLen) {
-                        /*
-                         * splitting
-                         * before
-                         *  abcd - ef
-                         *
-                         * insert "abc"
-                         *
-                         * after
-                         *  abc - d - ef
-                         */
-                        final int newNodeAddress = buffer.limit();
-                        final int flags = BinaryDictInputOutput.makeCharGroupFlags(p > 1,
-                                isTerminal, 0, hasShortcuts, hasBigrams, false /* isNotAWord */,
-                                false /* isBlackListEntry */, header.mFormatOptions);
-                        int written = moveGroup(newNodeAddress, currentInfo.mCharacters, p, flags,
-                                frequency, nodeParentAddress, shortcuts, bigrams, destination,
-                                buffer, nodeOriginAddress, address, header.mFormatOptions);
-
-                        final int[] characters2 = Arrays.copyOfRange(currentInfo.mCharacters, p,
-                                currentInfo.mCharacters.length);
-                        if (currentInfo.mChildrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) {
-                            updateParentAddresses(buffer, currentInfo.mChildrenAddress,
-                                    newNodeAddress + written + 1, header.mFormatOptions);
-                        }
-                        final CharGroupInfo newInfo2 = new CharGroupInfo(
-                                newNodeAddress + written + 1, -1 /* endAddress */,
-                                currentInfo.mFlags, characters2, currentInfo.mFrequency,
-                                newNodeAddress + 1, currentInfo.mChildrenAddress,
-                                currentInfo.mShortcutTargets, currentInfo.mBigrams);
-                        writeNode(destination, new CharGroupInfo[] { newInfo2 });
-                        return;
-                    } else if (codePoints[wordPos + p] != currentInfo.mCharacters[p]) {
-                        if (p > 0) {
-                            /*
-                             * splitting
-                             * before
-                             *   ab - cd
-                             *
-                             * insert "ac"
-                             *
-                             * after
-                             *   a - b - cd
-                             *     |
-                             *     - c
-                             */
-
-                            final int newNodeAddress = buffer.limit();
-                            final int childrenAddress = currentInfo.mChildrenAddress;
-
-                            // move prefix
-                            final int prefixFlags = BinaryDictInputOutput.makeCharGroupFlags(p > 1,
-                                    false /* isTerminal */, 0 /* childrenAddressSize*/,
-                                    false /* hasShortcut */, false /* hasBigrams */,
-                                    false /* isNotAWord */, false /* isBlackListEntry */,
-                                    header.mFormatOptions);
-                            int written = moveGroup(newNodeAddress, currentInfo.mCharacters, p,
-                                    prefixFlags, -1 /* frequency */, nodeParentAddress, null, null,
-                                    destination, buffer, nodeOriginAddress, address,
-                                    header.mFormatOptions);
-
-                            final int[] suffixCharacters = Arrays.copyOfRange(
-                                    currentInfo.mCharacters, p, currentInfo.mCharacters.length);
-                            if (currentInfo.mChildrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) {
-                                updateParentAddresses(buffer, currentInfo.mChildrenAddress,
-                                        newNodeAddress + written + 1, header.mFormatOptions);
-                            }
-                            final int suffixFlags = BinaryDictInputOutput.makeCharGroupFlags(
-                                    suffixCharacters.length > 1,
-                                    (currentInfo.mFlags & FormatSpec.FLAG_IS_TERMINAL) != 0,
-                                    0 /* childrenAddressSize */,
-                                    (currentInfo.mFlags & FormatSpec.FLAG_HAS_SHORTCUT_TARGETS)
-                                            != 0,
-                                    (currentInfo.mFlags & FormatSpec.FLAG_HAS_BIGRAMS) != 0,
-                                    isNotAWord, isBlackListEntry, header.mFormatOptions);
-                            final CharGroupInfo suffixInfo = new CharGroupInfo(
-                                    newNodeAddress + written + 1, -1 /* endAddress */, suffixFlags,
-                                    suffixCharacters, currentInfo.mFrequency, newNodeAddress + 1,
-                                    currentInfo.mChildrenAddress, currentInfo.mShortcutTargets,
-                                    currentInfo.mBigrams);
-                            written += computeGroupSize(suffixInfo, header.mFormatOptions) + 1;
-
-                            final int[] newCharacters = Arrays.copyOfRange(codePoints, wordPos + p,
-                                    codePoints.length);
-                            final int flags = BinaryDictInputOutput.makeCharGroupFlags(
-                                    newCharacters.length > 1, isTerminal,
-                                    0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
-                                    isNotAWord, isBlackListEntry, header.mFormatOptions);
-                            final CharGroupInfo newInfo = new CharGroupInfo(
-                                    newNodeAddress + written, -1 /* endAddress */, flags,
-                                    newCharacters, frequency, newNodeAddress + 1,
-                                    FormatSpec.NO_CHILDREN_ADDRESS, shortcuts, bigrams);
-                            writeNode(destination, new CharGroupInfo[] { suffixInfo, newInfo });
-                            return;
-                        }
-                        matched = false;
-                        break;
-                    }
-                }
-
-                if (matched) {
-                    if (wordPos + currentInfo.mCharacters.length == wordLen) {
-                        // the word exists in the dictionary.
-                        // only update group.
-                        final int newNodeAddress = buffer.limit();
-                        final boolean hasMultipleChars = currentInfo.mCharacters.length > 1;
-                        final int flags = BinaryDictInputOutput.makeCharGroupFlags(hasMultipleChars,
-                                isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
-                                isNotAWord, isBlackListEntry, header.mFormatOptions);
-                        final CharGroupInfo newInfo = new CharGroupInfo(newNodeAddress + 1,
-                                -1 /* endAddress */, flags, currentInfo.mCharacters, frequency,
-                                nodeParentAddress, currentInfo.mChildrenAddress, shortcuts,
-                                bigrams);
-                        moveCharGroup(destination, buffer, newInfo, nodeOriginAddress, address,
-                                header.mFormatOptions);
-                        return;
-                    }
-                    wordPos += currentInfo.mCharacters.length;
-                    if (currentInfo.mChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS) {
-                        /*
-                         * found the prefix of the word.
-                         * make new node and link to the node from this group.
-                         *
-                         * before
-                         * ab - cd
-                         *
-                         * insert "abcde"
-                         *
-                         * after
-                         * ab - cd - e
-                         */
-                        final int newNodeAddress = buffer.limit();
-                        updateChildrenAddress(buffer, address, newNodeAddress,
-                                header.mFormatOptions);
-                        final int newGroupAddress = newNodeAddress + 1;
-                        final boolean hasMultipleChars = (wordLen - wordPos) > 1;
-                        final int flags = BinaryDictInputOutput.makeCharGroupFlags(hasMultipleChars,
-                                isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
-                                isNotAWord, isBlackListEntry, header.mFormatOptions);
-                        final int[] characters = Arrays.copyOfRange(codePoints, wordPos, wordLen);
-                        final CharGroupInfo newInfo = new CharGroupInfo(newGroupAddress, -1, flags,
-                                characters, frequency, address, FormatSpec.NO_CHILDREN_ADDRESS,
-                                shortcuts, bigrams);
-                        writeNode(destination, new CharGroupInfo[] { newInfo });
-                        return;
-                    }
-                    buffer.position(currentInfo.mChildrenAddress);
-                    foundNextGroup = true;
-                    break;
-                }
-            }
-
-            if (foundNextGroup) continue;
-
-            // reached the end of the array.
-            final int linkAddressPosition = buffer.position();
-            int nextLink = buffer.readUnsignedInt24();
-            if ((nextLink & MSB24) != 0) {
-                nextLink = -(nextLink & SINT24_MAX);
-            }
-            if (nextLink == FormatSpec.NO_FORWARD_LINK_ADDRESS) {
-                /*
-                 * expand this node.
-                 *
-                 * before
-                 * ab - cd
-                 *
-                 * insert "abef"
-                 *
-                 * after
-                 * ab - cd
-                 *    |
-                 *    - ef
-                 */
-
-                // change the forward link address.
-                final int newNodeAddress = buffer.limit();
-                buffer.position(linkAddressPosition);
-                writeSInt24ToBuffer(buffer, newNodeAddress);
-
-                final int[] characters = Arrays.copyOfRange(codePoints, wordPos, wordLen);
-                final int flags = BinaryDictInputOutput.makeCharGroupFlags(characters.length > 1,
-                        isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
-                        isNotAWord, isBlackListEntry, header.mFormatOptions);
-                final CharGroupInfo newInfo = new CharGroupInfo(newNodeAddress + 1,
-                        -1 /* endAddress */, flags, characters, frequency, nodeParentAddress,
-                        FormatSpec.NO_CHILDREN_ADDRESS, shortcuts, bigrams);
-                writeNode(destination, new CharGroupInfo[]{ newInfo });
-                return;
-            } else {
-                depth--;
-                buffer.position(nextLink);
-            }
-        }
-    }
-
-    /**
      * Find a word from the buffer.
      *
      * @param buffer the buffer representing the body of the dictionary file.
@@ -1019,4 +564,52 @@
             return null;
         }
     }
+
+    /**
+     * Helper method to hide the actual value of the no children address.
+     */
+    public static boolean hasChildrenAddress(final int address) {
+        return FormatSpec.NO_CHILDREN_ADDRESS != address;
+    }
+
+    /**
+     * Helper method to check whether the group is moved.
+     */
+    public static boolean isMovedGroup(final int flags, final FormatOptions options) {
+        return options.mSupportsDynamicUpdate
+                && ((flags & FormatSpec.MASK_GROUP_ADDRESS_TYPE) == FormatSpec.FLAG_IS_MOVED);
+    }
+
+    /**
+     * Helper method to check whether the dictionary can be updated dynamically.
+     */
+    public static boolean supportsDynamicUpdate(final FormatOptions options) {
+        return options.mVersion >= FormatSpec.FIRST_VERSION_WITH_DYNAMIC_UPDATE
+                && options.mSupportsDynamicUpdate;
+    }
+
+    /**
+     * Helper method to check whether the group is deleted.
+     */
+    public static boolean isDeletedGroup(final int flags, final FormatOptions formatOptions) {
+        return formatOptions.mSupportsDynamicUpdate
+                && ((flags & FormatSpec.MASK_GROUP_ADDRESS_TYPE) == FormatSpec.FLAG_IS_DELETED);
+    }
+
+    /**
+     * Compute the binary size of the group count
+     * @param count the group count
+     * @return the size of the group count, either 1 or 2 bytes.
+     */
+    public static int getGroupCountSize(final int count) {
+        if (FormatSpec.MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= count) {
+            return 1;
+        } else if (FormatSpec.MAX_CHARGROUPS_IN_A_NODE >= count) {
+            return 2;
+        } else {
+            throw new RuntimeException("Can't have more than "
+                    + FormatSpec.MAX_CHARGROUPS_IN_A_NODE + " groups in a node (found " + count
+                    + ")");
+        }
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
index 504349a..a546610 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
@@ -307,29 +307,12 @@
     }
 
     /**
-     * Compute the binary size of the group count
-     * @param count the group count
-     * @return the size of the group count, either 1 or 2 bytes.
-     */
-    public static int getGroupCountSize(final int count) {
-        if (FormatSpec.MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= count) {
-            return 1;
-        } else if (FormatSpec.MAX_CHARGROUPS_IN_A_NODE >= count) {
-            return 2;
-        } else {
-            throw new RuntimeException("Can't have more than "
-                    + FormatSpec.MAX_CHARGROUPS_IN_A_NODE + " groups in a node (found " + count
-                    + ")");
-        }
-    }
-
-    /**
      * Compute the binary size of the group count for a node
      * @param node the node
      * @return the size of the group count, either 1 or 2 bytes.
      */
     private static int getGroupCountSize(final Node node) {
-        return getGroupCountSize(node.mData.size());
+        return BinaryDictIOUtils.getGroupCountSize(node.mData.size());
     }
 
     /**
@@ -404,44 +387,13 @@
     }
 
     /**
-     * Helper method to hide the actual value of the no children address.
-     */
-    public static boolean hasChildrenAddress(final int address) {
-        return FormatSpec.NO_CHILDREN_ADDRESS != address;
-    }
-
-    /**
-     * Helper method to check whether the group is moved.
-     */
-    public static boolean isMovedGroup(final int flags, final FormatOptions options) {
-        return options.mSupportsDynamicUpdate
-                && ((flags & FormatSpec.MASK_GROUP_ADDRESS_TYPE) == FormatSpec.FLAG_IS_MOVED);
-    }
-
-    /**
-     * Helper method to check whether the group is deleted.
-     */
-    public static boolean isDeletedGroup(final int flags, final FormatOptions formatOptions) {
-        return formatOptions.mSupportsDynamicUpdate
-                && ((flags & FormatSpec.MASK_GROUP_ADDRESS_TYPE) == FormatSpec.FLAG_IS_DELETED);
-    }
-
-    /**
-     * Helper method to check whether the dictionary can be updated dynamically.
-     */
-    public static boolean supportsDynamicUpdate(final FormatOptions options) {
-        return options.mVersion >= FormatSpec.FIRST_VERSION_WITH_DYNAMIC_UPDATE
-                && options.mSupportsDynamicUpdate;
-    }
-
-    /**
      * Compute the size of the header (flag + [parent address] + characters size) of a CharGroup.
      *
      * @param group the group of which to compute the size of the header
      * @param options file format options.
      */
     private static int getGroupHeaderSize(final CharGroup group, final FormatOptions options) {
-        if (supportsDynamicUpdate(options)) {
+        if (BinaryDictIOUtils.supportsDynamicUpdate(options)) {
             return FormatSpec.GROUP_FLAGS_SIZE + FormatSpec.PARENT_ADDRESS_SIZE
                     + getGroupCharactersSize(group);
         } else {
@@ -449,10 +401,6 @@
         }
     }
 
-    private static final int UINT8_MAX = 0xFF;
-    private static final int UINT16_MAX = 0xFFFF;
-    private static final int UINT24_MAX = 0xFFFFFF;
-
     /**
      * Compute the size, in bytes, that an address will occupy.
      *
@@ -464,22 +412,18 @@
      * @return the byte size.
      */
     static int getByteSize(final int address) {
-        assert(address <= UINT24_MAX);
-        if (!hasChildrenAddress(address)) {
+        assert(address <= FormatSpec.UINT24_MAX);
+        if (!BinaryDictIOUtils.hasChildrenAddress(address)) {
             return 0;
-        } else if (Math.abs(address) <= UINT8_MAX) {
+        } else if (Math.abs(address) <= FormatSpec.UINT8_MAX) {
             return 1;
-        } else if (Math.abs(address) <= UINT16_MAX) {
+        } else if (Math.abs(address) <= FormatSpec.UINT16_MAX) {
             return 2;
         } else {
             return 3;
         }
     }
 
-    private static final int SINT24_MAX = 0x7FFFFF;
-    private static final int MSB8 = 0x80;
-    private static final int MSB24 = 0x800000;
-
     // End utility methods.
 
     // This method is responsible for finding a nice ordering of the nodes that favors run-time
@@ -810,11 +754,12 @@
      */
     private static int writeVariableSignedAddress(final byte[] buffer, int index,
             final int address) {
-        if (!hasChildrenAddress(address)) {
+        if (!BinaryDictIOUtils.hasChildrenAddress(address)) {
             buffer[index] = buffer[index + 1] = buffer[index + 2] = 0;
         } else {
             final int absAddress = Math.abs(address);
-            buffer[index++] = (byte)((address < 0 ? MSB8 : 0) | (0xFF & (absAddress >> 16)));
+            buffer[index++] =
+                    (byte)((address < 0 ? FormatSpec.MSB8 : 0) | (0xFF & (absAddress >> 16)));
             buffer[index++] = (byte)(0xFF & (absAddress >> 8));
             buffer[index++] = (byte)(0xFF & absAddress);
         }
@@ -973,13 +918,13 @@
 
     private static final int writeParentAddress(final byte[] buffer, final int index,
             final int address, final FormatOptions formatOptions) {
-        if (supportsDynamicUpdate(formatOptions)) {
+        if (BinaryDictIOUtils.supportsDynamicUpdate(formatOptions)) {
             if (address == FormatSpec.NO_PARENT_ADDRESS) {
                 buffer[index] = buffer[index + 1] = buffer[index + 2] = 0;
             } else {
                 final int absAddress = Math.abs(address);
-                assert(absAddress <= SINT24_MAX);
-                buffer[index] = (byte)((address < 0 ? MSB8 : 0)
+                assert(absAddress <= FormatSpec.SINT24_MAX);
+                buffer[index] = (byte)((address < 0 ? FormatSpec.MSB8 : 0)
                         | ((absAddress >> 16) & 0xFF));
                 buffer[index + 1] = (byte)((absAddress >> 8) & 0xFF);
                 buffer[index + 2] = (byte)(absAddress & 0xFF);
@@ -1300,8 +1245,8 @@
         if (options.mSupportsDynamicUpdate) {
             final int address = buffer.readUnsignedInt24();
             if (address == 0) return FormatSpec.NO_CHILDREN_ADDRESS;
-            if ((address & MSB24) != 0) {
-                return -(address & SINT24_MAX);
+            if ((address & FormatSpec.MSB24) != 0) {
+                return -(address & FormatSpec.SINT24_MAX);
             } else {
                 return address;
             }
@@ -1322,10 +1267,10 @@
 
     static int readParentAddress(final FusionDictionaryBufferInterface buffer,
             final FormatOptions formatOptions) {
-        if (supportsDynamicUpdate(formatOptions)) {
+        if (BinaryDictIOUtils.supportsDynamicUpdate(formatOptions)) {
             final int parentAddress = buffer.readUnsignedInt24();
-            final int sign = ((parentAddress & MSB24) != 0) ? -1 : 1;
-            return sign * (parentAddress & SINT24_MAX);
+            final int sign = ((parentAddress & FormatSpec.MSB24) != 0) ? -1 : 1;
+            return sign * (parentAddress & FormatSpec.SINT24_MAX);
         } else {
             return FormatSpec.NO_PARENT_ADDRESS;
         }
@@ -1339,7 +1284,7 @@
         ++addressPointer;
 
         final int parentAddress = readParentAddress(buffer, options);
-        if (supportsDynamicUpdate(options)) {
+        if (BinaryDictIOUtils.supportsDynamicUpdate(options)) {
             addressPointer += 3;
         }
 
@@ -1466,7 +1411,7 @@
         final int originalPointer = buffer.position();
         buffer.position(address);
 
-        if (supportsDynamicUpdate(formatOptions)) {
+        if (BinaryDictIOUtils.supportsDynamicUpdate(formatOptions)) {
             result = getWordAtAddressWithParentAddress(buffer, headerSize, address, formatOptions);
         } else {
             result = getWordAtAddressWithoutParentAddress(buffer, headerSize, address,
@@ -1495,13 +1440,13 @@
             do {
                 buffer.position(currentAddress + headerSize);
                 currentInfo = readCharGroup(buffer, currentAddress, options);
-                if (isMovedGroup(currentInfo.mFlags, options)) {
+                if (BinaryDictIOUtils.isMovedGroup(currentInfo.mFlags, options)) {
                     currentAddress = currentInfo.mParentAddress + currentInfo.mOriginalAddress;
                 }
                 if (DBG && loopCounter++ > MAX_JUMPS) {
                     MakedictLog.d("Too many jumps - probably a bug");
                 }
-            } while (isMovedGroup(currentInfo.mFlags, options));
+            } while (BinaryDictIOUtils.isMovedGroup(currentInfo.mFlags, options));
             if (Integer.MIN_VALUE == frequency) frequency = currentInfo.mFrequency;
             for (int i = 0; i < currentInfo.mCharacters.length; ++i) {
                 sGetWordBuffer[index--] =
@@ -1521,7 +1466,7 @@
             final FormatOptions options) {
         buffer.position(headerSize);
         final int count = readCharGroupCount(buffer);
-        int groupOffset = getGroupCountSize(count);
+        int groupOffset = BinaryDictIOUtils.getGroupCountSize(count);
         final StringBuilder builder = new StringBuilder();
         WeightedString result = null;
 
@@ -1534,23 +1479,23 @@
                 result = new WeightedString(builder.toString(), info.mFrequency);
                 break; // and return
             }
-            if (hasChildrenAddress(info.mChildrenAddress)) {
+            if (BinaryDictIOUtils.hasChildrenAddress(info.mChildrenAddress)) {
                 if (info.mChildrenAddress > address) {
                     if (null == last) continue;
                     builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
                     buffer.position(last.mChildrenAddress + headerSize);
                     i = readCharGroupCount(buffer);
-                    groupOffset = last.mChildrenAddress + getGroupCountSize(i);
+                    groupOffset = last.mChildrenAddress + BinaryDictIOUtils.getGroupCountSize(i);
                     last = null;
                     continue;
                 }
                 last = info;
             }
-            if (0 == i && hasChildrenAddress(last.mChildrenAddress)) {
+            if (0 == i && BinaryDictIOUtils.hasChildrenAddress(last.mChildrenAddress)) {
                 builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
                 buffer.position(last.mChildrenAddress + headerSize);
                 i = readCharGroupCount(buffer);
-                groupOffset = last.mChildrenAddress + getGroupCountSize(i);
+                groupOffset = last.mChildrenAddress + BinaryDictIOUtils.getGroupCountSize(i);
                 last = null;
                 continue;
             }
@@ -1583,10 +1528,10 @@
         do { // Scan the linked-list node.
             final int nodeHeadPosition = buffer.position() - headerSize;
             final int count = readCharGroupCount(buffer);
-            int groupOffset = nodeHeadPosition + getGroupCountSize(count);
+            int groupOffset = nodeHeadPosition + BinaryDictIOUtils.getGroupCountSize(count);
             for (int i = count; i > 0; --i) { // Scan the array of CharGroup.
                 CharGroupInfo info = readCharGroup(buffer, groupOffset, options);
-                if (isMovedGroup(info.mFlags, options)) continue;
+                if (BinaryDictIOUtils.isMovedGroup(info.mFlags, options)) continue;
                 ArrayList<WeightedString> shortcutTargets = info.mShortcutTargets;
                 ArrayList<WeightedString> bigrams = null;
                 if (null != info.mBigrams) {
@@ -1599,7 +1544,7 @@
                         bigrams.add(new WeightedString(word.mWord, reconstructedFrequency));
                     }
                 }
-                if (hasChildrenAddress(info.mChildrenAddress)) {
+                if (BinaryDictIOUtils.hasChildrenAddress(info.mChildrenAddress)) {
                     Node children = reverseNodeMap.get(info.mChildrenAddress);
                     if (null == children) {
                         final int currentPosition = buffer.position();
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictReader.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictReader.java
index 57a5832..a4a7ce4 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictReader.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictReader.java
@@ -24,6 +24,7 @@
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.RandomAccessFile;
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
 
@@ -82,6 +83,32 @@
         }
     }
 
+    /**
+     * Creates FusionDictionaryBuffer from a RandomAccessFile.
+     */
+    @UsedForTesting
+    public static final class FusionDictionaryBufferFromWritableByteBufferFactory
+            implements FusionDictionaryBufferFactory {
+        @Override
+        public FusionDictionaryBufferInterface getFusionDictionaryBuffer(final File file)
+                throws FileNotFoundException, IOException {
+            RandomAccessFile raFile = null;
+            ByteBuffer buffer = null;
+            try {
+                raFile = new RandomAccessFile(file, "rw");
+                buffer = raFile.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, file.length());
+            } finally {
+                if (raFile != null) {
+                    raFile.close();
+                }
+            }
+            if (buffer != null) {
+                return new BinaryDictInputOutput.ByteBufferWrapper(buffer);
+            }
+            return null;
+        }
+    }
+
     private final File mDictionaryBinaryFile;
     private FusionDictionaryBufferInterface mFusionDictionaryBuffer;
 
diff --git a/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java
new file mode 100644
index 0000000..5b10912
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java
@@ -0,0 +1,493 @@
+/*
+ * 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.Constants;
+import com.android.inputmethod.latin.makedict.BinaryDictInputOutput.FusionDictionaryBufferInterface;
+import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
+import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
+import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * The utility class to help dynamic updates on the binary dictionary.
+ *
+ * All the methods in this class are static.
+ */
+@UsedForTesting
+public final class DynamicBinaryDictIOUtils {
+    private static final boolean DBG = false;
+    private static final int MAX_JUMPS = 10000;
+
+    private DynamicBinaryDictIOUtils() {
+        // This utility class is not publicly instantiable.
+    }
+
+    private static int markAsDeleted(final int flags) {
+        return (flags & (~FormatSpec.MASK_GROUP_ADDRESS_TYPE)) | FormatSpec.FLAG_IS_DELETED;
+    }
+
+    /**
+     * Delete the word from the binary file.
+     *
+     * @param buffer the buffer to write.
+     * @param word the word we delete
+     * @throws IOException
+     * @throws UnsupportedFormatException
+     */
+    @UsedForTesting
+    public static void deleteWord(final FusionDictionaryBufferInterface buffer,
+            final String word) throws IOException, UnsupportedFormatException {
+        buffer.position(0);
+        final FileHeader header = BinaryDictInputOutput.readHeader(buffer);
+        final int wordPosition = BinaryDictIOUtils.getTerminalPosition(buffer, word);
+        if (wordPosition == FormatSpec.NOT_VALID_WORD) return;
+
+        buffer.position(wordPosition);
+        final int flags = buffer.readUnsignedByte();
+        buffer.position(wordPosition);
+        buffer.put((byte)markAsDeleted(flags));
+    }
+
+    /**
+     * Update a parent address in a CharGroup that is referred to by groupOriginAddress.
+     *
+     * @param buffer the buffer to write.
+     * @param groupOriginAddress the address of the group.
+     * @param newParentAddress the absolute address of the parent.
+     * @param formatOptions file format options.
+     */
+    public static void updateParentAddress(final FusionDictionaryBufferInterface buffer,
+            final int groupOriginAddress, final int newParentAddress,
+            final FormatOptions formatOptions) {
+        final int originalPosition = buffer.position();
+        buffer.position(groupOriginAddress);
+        if (!formatOptions.mSupportsDynamicUpdate) {
+            throw new RuntimeException("this file format does not support parent addresses");
+        }
+        final int flags = buffer.readUnsignedByte();
+        if (BinaryDictIOUtils.isMovedGroup(flags, formatOptions)) {
+            // if the group is moved, the parent address is stored in the destination group.
+            // We are guaranteed to process the destination group later, so there is no need to
+            // update anything here.
+            buffer.position(originalPosition);
+            return;
+        }
+        if (DBG) {
+            MakedictLog.d("update parent address flags=" + flags + ", " + groupOriginAddress);
+        }
+        final int parentOffset = newParentAddress - groupOriginAddress;
+        BinaryDictIOUtils.writeSInt24ToBuffer(buffer, parentOffset);
+        buffer.position(originalPosition);
+    }
+
+    /**
+     * Update parent addresses in a Node that is referred to by nodeOriginAddress.
+     *
+     * @param buffer the buffer to be modified.
+     * @param nodeOriginAddress the address of a modified Node.
+     * @param newParentAddress the address to be written.
+     * @param formatOptions file format options.
+     */
+    public static void updateParentAddresses(final FusionDictionaryBufferInterface buffer,
+            final int nodeOriginAddress, final int newParentAddress,
+            final FormatOptions formatOptions) {
+        final int originalPosition = buffer.position();
+        buffer.position(nodeOriginAddress);
+        do {
+            final int count = BinaryDictInputOutput.readCharGroupCount(buffer);
+            for (int i = 0; i < count; ++i) {
+                updateParentAddress(buffer, buffer.position(), newParentAddress, formatOptions);
+                BinaryDictIOUtils.skipCharGroup(buffer, formatOptions);
+            }
+            final int forwardLinkAddress = buffer.readUnsignedInt24();
+            buffer.position(forwardLinkAddress);
+        } while (formatOptions.mSupportsDynamicUpdate
+                && buffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS);
+        buffer.position(originalPosition);
+    }
+
+    /**
+     * Update a children address in a CharGroup that is addressed by groupOriginAddress.
+     *
+     * @param buffer the buffer to write.
+     * @param groupOriginAddress the address of the group.
+     * @param newChildrenAddress the absolute address of the child.
+     * @param formatOptions file format options.
+     */
+    public static void updateChildrenAddress(final FusionDictionaryBufferInterface buffer,
+            final int groupOriginAddress, final int newChildrenAddress,
+            final FormatOptions formatOptions) {
+        final int originalPosition = buffer.position();
+        buffer.position(groupOriginAddress);
+        final int flags = buffer.readUnsignedByte();
+        final int parentAddress = BinaryDictInputOutput.readParentAddress(buffer, formatOptions);
+        BinaryDictIOUtils.skipString(buffer, (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS) != 0);
+        if ((flags & FormatSpec.FLAG_IS_TERMINAL) != 0) buffer.readUnsignedByte();
+        final int childrenOffset = newChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS
+                ? FormatSpec.NO_CHILDREN_ADDRESS : newChildrenAddress - buffer.position();
+        BinaryDictIOUtils.writeSInt24ToBuffer(buffer, childrenOffset);
+        buffer.position(originalPosition);
+    }
+
+    /**
+     * Helper method to move a char group to the tail of the file.
+     */
+    private static int moveCharGroup(final OutputStream destination,
+            final FusionDictionaryBufferInterface buffer, final CharGroupInfo info,
+            final int nodeOriginAddress, final int oldGroupAddress,
+            final FormatOptions formatOptions) throws IOException {
+        updateParentAddress(buffer, oldGroupAddress, buffer.limit() + 1, formatOptions);
+        buffer.position(oldGroupAddress);
+        final int currentFlags = buffer.readUnsignedByte();
+        buffer.position(oldGroupAddress);
+        buffer.put((byte)(FormatSpec.FLAG_IS_MOVED | (currentFlags
+                & (~FormatSpec.MASK_MOVE_AND_DELETE_FLAG))));
+        int size = FormatSpec.GROUP_FLAGS_SIZE;
+        updateForwardLink(buffer, nodeOriginAddress, buffer.limit(), formatOptions);
+        size += BinaryDictIOUtils.writeNode(destination, new CharGroupInfo[] { info });
+        return size;
+    }
+    @SuppressWarnings("unused")
+    private static void updateForwardLink(final FusionDictionaryBufferInterface buffer,
+            final int nodeOriginAddress, final int newNodeAddress,
+            final FormatOptions formatOptions) {
+        buffer.position(nodeOriginAddress);
+        int jumpCount = 0;
+        while (jumpCount++ < MAX_JUMPS) {
+            final int count = BinaryDictInputOutput.readCharGroupCount(buffer);
+            for (int i = 0; i < count; ++i) BinaryDictIOUtils.skipCharGroup(buffer, formatOptions);
+            final int forwardLinkAddress = buffer.readUnsignedInt24();
+            if (forwardLinkAddress == FormatSpec.NO_FORWARD_LINK_ADDRESS) {
+                buffer.position(buffer.position() - FormatSpec.FORWARD_LINK_ADDRESS_SIZE);
+                BinaryDictIOUtils.writeSInt24ToBuffer(buffer, newNodeAddress);
+                return;
+            }
+            buffer.position(forwardLinkAddress);
+        }
+        if (DBG && jumpCount >= MAX_JUMPS) {
+            throw new RuntimeException("too many jumps, probably a bug.");
+        }
+    }
+
+    /**
+     * Move a group that is referred to by oldGroupOrigin to the tail of the file.
+     * And set the children address to the byte after the group.
+     *
+     * @param nodeOrigin the address of the tail of the file.
+     * @param characters
+     * @param length
+     * @param flags
+     * @param frequency
+     * @param parentAddress
+     * @param shortcutTargets
+     * @param bigrams
+     * @param destination the stream representing the tail of the file.
+     * @param buffer the buffer representing the (constant-size) body of the file.
+     * @param oldNodeOrigin
+     * @param oldGroupOrigin
+     * @param formatOptions
+     * @return the size written, in bytes.
+     * @throws IOException
+     */
+    private static int moveGroup(final int nodeOrigin, final int[] characters, final int length,
+            final int flags, final int frequency, final int parentAddress,
+            final ArrayList<WeightedString> shortcutTargets,
+            final ArrayList<PendingAttribute> bigrams, final OutputStream destination,
+            final FusionDictionaryBufferInterface buffer, final int oldNodeOrigin,
+            final int oldGroupOrigin, final FormatOptions formatOptions) throws IOException {
+        int size = 0;
+        final int newGroupOrigin = nodeOrigin + 1;
+        final int[] writtenCharacters = Arrays.copyOfRange(characters, 0, length);
+        final CharGroupInfo tmpInfo = new CharGroupInfo(newGroupOrigin, -1 /* endAddress */,
+                flags, writtenCharacters, frequency, parentAddress, FormatSpec.NO_CHILDREN_ADDRESS,
+                shortcutTargets, bigrams);
+        size = BinaryDictIOUtils.computeGroupSize(tmpInfo, formatOptions);
+        final CharGroupInfo newInfo = new CharGroupInfo(newGroupOrigin, newGroupOrigin + size,
+                flags, writtenCharacters, frequency, parentAddress,
+                nodeOrigin + 1 + size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE, shortcutTargets,
+                bigrams);
+        moveCharGroup(destination, buffer, newInfo, oldNodeOrigin, oldGroupOrigin, formatOptions);
+        return 1 + size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE;
+    }
+
+    /**
+     * Insert a word into a binary dictionary.
+     *
+     * @param buffer
+     * @param destination
+     * @param word
+     * @param frequency
+     * @param bigramStrings
+     * @param shortcuts
+     * @throws IOException
+     * @throws UnsupportedFormatException
+     */
+    // TODO: Support batch insertion.
+    // TODO: Remove @UsedForTesting once UserHistoryDictionary is implemented by BinaryDictionary.
+    @UsedForTesting
+    public static void insertWord(final FusionDictionaryBufferInterface buffer,
+            final OutputStream destination, final String word, final int frequency,
+            final ArrayList<WeightedString> bigramStrings,
+            final ArrayList<WeightedString> shortcuts, final boolean isNotAWord,
+            final boolean isBlackListEntry)
+                    throws IOException, UnsupportedFormatException {
+        final ArrayList<PendingAttribute> bigrams = new ArrayList<PendingAttribute>();
+        if (bigramStrings != null) {
+            for (final WeightedString bigram : bigramStrings) {
+                int position = BinaryDictIOUtils.getTerminalPosition(buffer, bigram.mWord);
+                if (position == FormatSpec.NOT_VALID_WORD) {
+                    // TODO: figure out what is the correct thing to do here.
+                } else {
+                    bigrams.add(new PendingAttribute(bigram.mFrequency, position));
+                }
+            }
+        }
+
+        final boolean isTerminal = true;
+        final boolean hasBigrams = !bigrams.isEmpty();
+        final boolean hasShortcuts = shortcuts != null && !shortcuts.isEmpty();
+
+        // find the insert position of the word.
+        if (buffer.position() != 0) buffer.position(0);
+        final FileHeader header = BinaryDictInputOutput.readHeader(buffer);
+
+        int wordPos = 0, address = buffer.position(), nodeOriginAddress = buffer.position();
+        final int[] codePoints = FusionDictionary.getCodePoints(word);
+        final int wordLen = codePoints.length;
+
+        for (int depth = 0; depth < Constants.DICTIONARY_MAX_WORD_LENGTH; ++depth) {
+            if (wordPos >= wordLen) break;
+            nodeOriginAddress = buffer.position();
+            int nodeParentAddress = -1;
+            final int charGroupCount = BinaryDictInputOutput.readCharGroupCount(buffer);
+            boolean foundNextGroup = false;
+
+            for (int i = 0; i < charGroupCount; ++i) {
+                address = buffer.position();
+                final CharGroupInfo currentInfo = BinaryDictInputOutput.readCharGroup(buffer,
+                        buffer.position(), header.mFormatOptions);
+                final boolean isMovedGroup = BinaryDictIOUtils.isMovedGroup(currentInfo.mFlags,
+                        header.mFormatOptions);
+                if (isMovedGroup) continue;
+                nodeParentAddress = (currentInfo.mParentAddress == FormatSpec.NO_PARENT_ADDRESS)
+                        ? FormatSpec.NO_PARENT_ADDRESS : currentInfo.mParentAddress + address;
+                boolean matched = true;
+                for (int p = 0; p < currentInfo.mCharacters.length; ++p) {
+                    if (wordPos + p >= wordLen) {
+                        /*
+                         * splitting
+                         * before
+                         *  abcd - ef
+                         *
+                         * insert "abc"
+                         *
+                         * after
+                         *  abc - d - ef
+                         */
+                        final int newNodeAddress = buffer.limit();
+                        final int flags = BinaryDictInputOutput.makeCharGroupFlags(p > 1,
+                                isTerminal, 0, hasShortcuts, hasBigrams, false /* isNotAWord */,
+                                false /* isBlackListEntry */, header.mFormatOptions);
+                        int written = moveGroup(newNodeAddress, currentInfo.mCharacters, p, flags,
+                                frequency, nodeParentAddress, shortcuts, bigrams, destination,
+                                buffer, nodeOriginAddress, address, header.mFormatOptions);
+
+                        final int[] characters2 = Arrays.copyOfRange(currentInfo.mCharacters, p,
+                                currentInfo.mCharacters.length);
+                        if (currentInfo.mChildrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) {
+                            updateParentAddresses(buffer, currentInfo.mChildrenAddress,
+                                    newNodeAddress + written + 1, header.mFormatOptions);
+                        }
+                        final CharGroupInfo newInfo2 = new CharGroupInfo(
+                                newNodeAddress + written + 1, -1 /* endAddress */,
+                                currentInfo.mFlags, characters2, currentInfo.mFrequency,
+                                newNodeAddress + 1, currentInfo.mChildrenAddress,
+                                currentInfo.mShortcutTargets, currentInfo.mBigrams);
+                        BinaryDictIOUtils.writeNode(destination, new CharGroupInfo[] { newInfo2 });
+                        return;
+                    } else if (codePoints[wordPos + p] != currentInfo.mCharacters[p]) {
+                        if (p > 0) {
+                            /*
+                             * splitting
+                             * before
+                             *   ab - cd
+                             *
+                             * insert "ac"
+                             *
+                             * after
+                             *   a - b - cd
+                             *     |
+                             *     - c
+                             */
+
+                            final int newNodeAddress = buffer.limit();
+                            final int childrenAddress = currentInfo.mChildrenAddress;
+
+                            // move prefix
+                            final int prefixFlags = BinaryDictInputOutput.makeCharGroupFlags(p > 1,
+                                    false /* isTerminal */, 0 /* childrenAddressSize*/,
+                                    false /* hasShortcut */, false /* hasBigrams */,
+                                    false /* isNotAWord */, false /* isBlackListEntry */,
+                                    header.mFormatOptions);
+                            int written = moveGroup(newNodeAddress, currentInfo.mCharacters, p,
+                                    prefixFlags, -1 /* frequency */, nodeParentAddress, null, null,
+                                    destination, buffer, nodeOriginAddress, address,
+                                    header.mFormatOptions);
+
+                            final int[] suffixCharacters = Arrays.copyOfRange(
+                                    currentInfo.mCharacters, p, currentInfo.mCharacters.length);
+                            if (currentInfo.mChildrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) {
+                                updateParentAddresses(buffer, currentInfo.mChildrenAddress,
+                                        newNodeAddress + written + 1, header.mFormatOptions);
+                            }
+                            final int suffixFlags = BinaryDictInputOutput.makeCharGroupFlags(
+                                    suffixCharacters.length > 1,
+                                    (currentInfo.mFlags & FormatSpec.FLAG_IS_TERMINAL) != 0,
+                                    0 /* childrenAddressSize */,
+                                    (currentInfo.mFlags & FormatSpec.FLAG_HAS_SHORTCUT_TARGETS)
+                                            != 0,
+                                    (currentInfo.mFlags & FormatSpec.FLAG_HAS_BIGRAMS) != 0,
+                                    isNotAWord, isBlackListEntry, header.mFormatOptions);
+                            final CharGroupInfo suffixInfo = new CharGroupInfo(
+                                    newNodeAddress + written + 1, -1 /* endAddress */, suffixFlags,
+                                    suffixCharacters, currentInfo.mFrequency, newNodeAddress + 1,
+                                    currentInfo.mChildrenAddress, currentInfo.mShortcutTargets,
+                                    currentInfo.mBigrams);
+                            written += BinaryDictIOUtils.computeGroupSize(suffixInfo,
+                                    header.mFormatOptions) + 1;
+
+                            final int[] newCharacters = Arrays.copyOfRange(codePoints, wordPos + p,
+                                    codePoints.length);
+                            final int flags = BinaryDictInputOutput.makeCharGroupFlags(
+                                    newCharacters.length > 1, isTerminal,
+                                    0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
+                                    isNotAWord, isBlackListEntry, header.mFormatOptions);
+                            final CharGroupInfo newInfo = new CharGroupInfo(
+                                    newNodeAddress + written, -1 /* endAddress */, flags,
+                                    newCharacters, frequency, newNodeAddress + 1,
+                                    FormatSpec.NO_CHILDREN_ADDRESS, shortcuts, bigrams);
+                            BinaryDictIOUtils.writeNode(destination,
+                                    new CharGroupInfo[] { suffixInfo, newInfo });
+                            return;
+                        }
+                        matched = false;
+                        break;
+                    }
+                }
+
+                if (matched) {
+                    if (wordPos + currentInfo.mCharacters.length == wordLen) {
+                        // the word exists in the dictionary.
+                        // only update group.
+                        final int newNodeAddress = buffer.limit();
+                        final boolean hasMultipleChars = currentInfo.mCharacters.length > 1;
+                        final int flags = BinaryDictInputOutput.makeCharGroupFlags(hasMultipleChars,
+                                isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
+                                isNotAWord, isBlackListEntry, header.mFormatOptions);
+                        final CharGroupInfo newInfo = new CharGroupInfo(newNodeAddress + 1,
+                                -1 /* endAddress */, flags, currentInfo.mCharacters, frequency,
+                                nodeParentAddress, currentInfo.mChildrenAddress, shortcuts,
+                                bigrams);
+                        moveCharGroup(destination, buffer, newInfo, nodeOriginAddress, address,
+                                header.mFormatOptions);
+                        return;
+                    }
+                    wordPos += currentInfo.mCharacters.length;
+                    if (currentInfo.mChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS) {
+                        /*
+                         * found the prefix of the word.
+                         * make new node and link to the node from this group.
+                         *
+                         * before
+                         * ab - cd
+                         *
+                         * insert "abcde"
+                         *
+                         * after
+                         * ab - cd - e
+                         */
+                        final int newNodeAddress = buffer.limit();
+                        updateChildrenAddress(buffer, address, newNodeAddress,
+                                header.mFormatOptions);
+                        final int newGroupAddress = newNodeAddress + 1;
+                        final boolean hasMultipleChars = (wordLen - wordPos) > 1;
+                        final int flags = BinaryDictInputOutput.makeCharGroupFlags(hasMultipleChars,
+                                isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
+                                isNotAWord, isBlackListEntry, header.mFormatOptions);
+                        final int[] characters = Arrays.copyOfRange(codePoints, wordPos, wordLen);
+                        final CharGroupInfo newInfo = new CharGroupInfo(newGroupAddress, -1, flags,
+                                characters, frequency, address, FormatSpec.NO_CHILDREN_ADDRESS,
+                                shortcuts, bigrams);
+                        BinaryDictIOUtils.writeNode(destination, new CharGroupInfo[] { newInfo });
+                        return;
+                    }
+                    buffer.position(currentInfo.mChildrenAddress);
+                    foundNextGroup = true;
+                    break;
+                }
+            }
+
+            if (foundNextGroup) continue;
+
+            // reached the end of the array.
+            final int linkAddressPosition = buffer.position();
+            int nextLink = buffer.readUnsignedInt24();
+            if ((nextLink & FormatSpec.MSB24) != 0) {
+                nextLink = -(nextLink & FormatSpec.SINT24_MAX);
+            }
+            if (nextLink == FormatSpec.NO_FORWARD_LINK_ADDRESS) {
+                /*
+                 * expand this node.
+                 *
+                 * before
+                 * ab - cd
+                 *
+                 * insert "abef"
+                 *
+                 * after
+                 * ab - cd
+                 *    |
+                 *    - ef
+                 */
+
+                // change the forward link address.
+                final int newNodeAddress = buffer.limit();
+                buffer.position(linkAddressPosition);
+                BinaryDictIOUtils.writeSInt24ToBuffer(buffer, newNodeAddress);
+
+                final int[] characters = Arrays.copyOfRange(codePoints, wordPos, wordLen);
+                final int flags = BinaryDictInputOutput.makeCharGroupFlags(characters.length > 1,
+                        isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
+                        isNotAWord, isBlackListEntry, header.mFormatOptions);
+                final CharGroupInfo newInfo = new CharGroupInfo(newNodeAddress + 1,
+                        -1 /* endAddress */, flags, characters, frequency, nodeParentAddress,
+                        FormatSpec.NO_CHILDREN_ADDRESS, shortcuts, bigrams);
+                BinaryDictIOUtils.writeNode(destination, new CharGroupInfo[]{ newInfo });
+                return;
+            } else {
+                depth--;
+                buffer.position(nextLink);
+            }
+        }
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
index 2bb5d8b..9af66ed 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
@@ -263,6 +263,13 @@
     static final int NOT_VALID_WORD = -99;
     static final int SIGNED_CHILDREN_ADDRESS_SIZE = 3;
 
+    static final int UINT8_MAX = 0xFF;
+    static final int UINT16_MAX = 0xFFFF;
+    static final int UINT24_MAX = 0xFFFFFF;
+    static final int SINT24_MAX = 0x7FFFFF;
+    static final int MSB8 = 0x80;
+    static final int MSB24 = 0x800000;
+
     /**
      * Options about file format.
      */
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java
index e9dbbc2..a755f90 100644
--- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java
@@ -70,7 +70,17 @@
         unsetPredictionDictionary();
     }
 
-    public void addToPersonalizationDictionary(
+    public void addBigramToPersonalizationDictionary(String word0, String word1, boolean isValid,
+            int frequency) {
+        final DynamicPredictionDictionaryBase dictionary = getPredictionDictionary();
+        if (dictionary == null) {
+            return;
+        }
+        dictionary.addToPersonalizationPredictionDictionary(word0, word1, isValid);
+    }
+
+    // Bulk import
+    public void addBigramsToPersonalizationDictionary(
             final ArrayList<PersonalizationLanguageModelParam> lmParams) {
         final DynamicPredictionDictionaryBase dictionary = getPredictionDictionary();
         if (dictionary == null) {
diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
index a25cf62..195f9f8 100644
--- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
@@ -22,6 +22,7 @@
 import android.util.Log;
 import android.view.inputmethod.EditorInfo;
 
+import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.keyboard.internal.KeySpecParser;
 import com.android.inputmethod.latin.Dictionary;
 import com.android.inputmethod.latin.InputAttributes;
@@ -170,6 +171,55 @@
         mIsInternal = Settings.isInternal(prefs);
     }
 
+    // Only for tests
+    private SettingsValues(final Locale locale) {
+        // TODO: locale is saved, but not used yet. May have to change this if tests require.
+        mLocale = locale;
+        mDelayUpdateOldSuggestions = 0;
+        mSymbolsPrecededBySpace = new int[] { '(', '[', '{', '&' };
+        Arrays.sort(mSymbolsPrecededBySpace);
+        mSymbolsFollowedBySpace = new int[] { '.', ',', ';', ':', '!', '?', ')', ']', '}', '&' };
+        Arrays.sort(mSymbolsFollowedBySpace);
+        mWordConnectors = new int[] { '\'', '-' };
+        Arrays.sort(mWordConnectors);
+        final String[] suggestPuncsSpec = new String[] { "!", "?", ",", ":", ";" };
+        mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec);
+        mWordSeparators = "&\t \n()[]{}*&<>+=|.,;:!?/_\"";
+        mHintToSaveText = "Touch again to save";
+        mInputAttributes = new InputAttributes(null, false /* isFullscreenMode */);
+        mAutoCap = true;
+        mVibrateOn = true;
+        mSoundOn = true;
+        mKeyPreviewPopupOn = true;
+        mSlidingKeyInputPreviewEnabled = true;
+        mVoiceMode = "0";
+        mIncludesOtherImesInLanguageSwitchList = false;
+        mShowsLanguageSwitchKey = true;
+        mUseContactsDict = true;
+        mUseDoubleSpacePeriod = true;
+        mBlockPotentiallyOffensive = true;
+        mAutoCorrectEnabled = true;
+        mBigramPredictionEnabled = true;
+        mKeyLongpressTimeout = 300;
+        mKeypressVibrationDuration = 5;
+        mKeypressSoundVolume = 1;
+        mKeyPreviewPopupDismissDelay = 70;
+        mAutoCorrectionThreshold = 1;
+        mVoiceKeyEnabled = true;
+        mVoiceKeyOnMain = true;
+        mGestureInputEnabled = true;
+        mGestureTrailEnabled = true;
+        mGestureFloatingPreviewTextEnabled = true;
+        mCorrectionEnabled = mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect;
+        mSuggestionVisibility = 0;
+        mIsInternal = false;
+    }
+
+    @UsedForTesting
+    public static SettingsValues makeDummySettingsValuesForTest(final Locale locale) {
+        return new SettingsValues(locale);
+    }
+
     public boolean isApplicationSpecifiedCompletionsOn() {
         return mInputAttributes.mApplicationSpecifiedCompletionOn;
     }
@@ -194,6 +244,10 @@
         return Arrays.binarySearch(mWordConnectors, code) >= 0;
     }
 
+    public boolean isWordCodePoint(final int code) {
+        return Character.isLetter(code) || isWordConnector(code);
+    }
+
     public boolean isUsuallyPrecededBySpace(final int code) {
         return Arrays.binarySearch(mSymbolsPrecededBySpace, code) >= 0;
     }
diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
index e97069d..acd4745 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
@@ -210,7 +210,8 @@
                 final int indexInMoreSuggestions = index + SUGGESTION_CODE_BASE;
                 final Key key = new Key(
                         params, word, info, KeyboardIconsSet.ICON_UNDEFINED, indexInMoreSuggestions,
-                        null, x, y, width, params.mDefaultRowHeight, 0);
+                        null /* outputText */, x, y, width, params.mDefaultRowHeight,
+                        0 /* labelFlags */, Key.BACKGROUND_TYPE_NORMAL);
                 params.markAsEdgeKey(key, index);
                 params.onAddKey(key);
                 final int columnNumber = params.getColumnNumber(index);
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
index 2644f3c..badc942 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
@@ -198,7 +198,7 @@
 
     @Override
     public boolean onLongClick(final View view) {
-        AudioAndHapticFeedbackManager.getInstance().hapticAndAudioFeedback(
+        AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback(
                 Constants.NOT_A_CODE, this);
         return showMoreSuggestions();
     }
diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
index 7406d85..0b4838c 100644
--- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
@@ -18,7 +18,9 @@
 
 import android.text.TextUtils;
 
+import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.settings.SettingsValues;
 
 import java.util.ArrayList;
 import java.util.Locale;
@@ -193,27 +195,56 @@
     }
 
     public static boolean isIdenticalAfterUpcase(final String text) {
-        final int len = text.length();
-        for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) {
+        final int length = text.length();
+        int i = 0;
+        while (i < length) {
             final int codePoint = text.codePointAt(i);
             if (Character.isLetter(codePoint) && !Character.isUpperCase(codePoint)) {
                 return false;
             }
+            i += Character.charCount(codePoint);
         }
         return true;
     }
 
     public static boolean isIdenticalAfterDowncase(final String text) {
-        final int len = text.length();
-        for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) {
+        final int length = text.length();
+        int i = 0;
+        while (i < length) {
             final int codePoint = text.codePointAt(i);
             if (Character.isLetter(codePoint) && !Character.isLowerCase(codePoint)) {
                 return false;
             }
+            i += Character.charCount(codePoint);
         }
         return true;
     }
 
+    @UsedForTesting
+    public static boolean looksValidForDictionaryInsertion(final CharSequence text,
+            final SettingsValues settings) {
+        if (TextUtils.isEmpty(text)) return false;
+        final int length = text.length();
+        int i = 0;
+        int digitCount = 0;
+        while (i < length) {
+            final int codePoint = Character.codePointAt(text, i);
+            final int charCount = Character.charCount(codePoint);
+            i += charCount;
+            if (Character.isDigit(codePoint)) {
+                // Count digits: see below
+                digitCount += charCount;
+                continue;
+            }
+            if (!settings.isWordCodePoint(codePoint)) return false;
+        }
+        // We reject strings entirely comprised of digits to avoid using PIN codes or credit
+        // card numbers. It would come in handy for word prediction though; a good example is
+        // when writing one's address where the street number is usually quite discriminative,
+        // as well as the postal code.
+        return digitCount < length;
+    }
+
     public static boolean isIdenticalAfterCapitalizeEachWord(final String text,
             final String separators) {
         boolean needCapsNext = true;
@@ -316,4 +347,14 @@
         // Otherwise, it doesn't look like an URL.
         return false;
     }
+
+    public static boolean isEmptyStringOrWhiteSpaces(String s) {
+        final int N = codePointCount(s);
+        for (int i = 0; i < N; ++i) {
+            if (!Character.isWhitespace(s.codePointAt(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
 }
diff --git a/native/jni/Android.mk b/native/jni/Android.mk
index a0c1c68..6e1d765 100644
--- a/native/jni/Android.mk
+++ b/native/jni/Android.mk
@@ -53,9 +53,7 @@
         dic_nodes_cache.cpp) \
     $(addprefix suggest/core/dictionary/, \
         bigram_dictionary.cpp \
-        binary_dictionary_format_utils.cpp \
         bloom_filter.cpp \
-        byte_array_utils.cpp \
         dictionary.cpp \
         digraph_utils.cpp \
         multi_bigram_map.cpp) \
@@ -72,6 +70,8 @@
         header/header_policy.cpp \
         header/header_reading_utils.cpp \
         shortcut/shortcut_list_reading_utils.cpp \
+        utils/byte_array_utils.cpp \
+        utils/format_utils.cpp \
         dictionary_structure_with_buffer_policy_factory.cpp \
         dynamic_patricia_trie_node_reader.cpp \
         dynamic_patricia_trie_policy.cpp \
diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
index 28d93b6..c895a86 100644
--- a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
+++ b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
@@ -27,10 +27,10 @@
 #include "defines.h"
 #include "jni.h"
 #include "jni_common.h"
-#include "suggest/core/dictionary/binary_dictionary_format_utils.h"
 #include "suggest/core/dictionary/binary_dictionary_info.h"
 #include "suggest/core/dictionary/dictionary.h"
 #include "suggest/core/suggest_options.h"
+#include "suggest/policyimpl/dictionary/utils/format_utils.h"
 #include "utils/autocorrection_threshold_utils.h"
 
 namespace latinime {
@@ -88,8 +88,8 @@
         return 0;
     }
     Dictionary *dictionary = 0;
-    if (BinaryDictionaryFormatUtils::UNKNOWN_VERSION
-            == BinaryDictionaryFormatUtils::detectFormatVersion(static_cast<uint8_t *>(dictBuf),
+    if (FormatUtils::UNKNOWN_VERSION
+            == FormatUtils::detectFormatVersion(static_cast<uint8_t *>(dictBuf),
                     static_cast<int>(dictSize))) {
         AKLOGE("DICT: dictionary format is unknown, bad magic number");
         releaseDictBuf(static_cast<const char *>(dictBuf) - offset, adjDictSize, fd);
diff --git a/native/jni/src/suggest/core/dicnode/dic_node_proximity_filter.h b/native/jni/src/suggest/core/dicnode/dic_node_proximity_filter.h
deleted file mode 100644
index c7ab571..0000000
--- a/native/jni/src/suggest/core/dicnode/dic_node_proximity_filter.h
+++ /dev/null
@@ -1,58 +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.
- */
-
-#ifndef LATINIME_DIC_NODE_PROXIMITY_FILTER_H
-#define LATINIME_DIC_NODE_PROXIMITY_FILTER_H
-
-#include "defines.h"
-#include "suggest/core/layout/proximity_info_state.h"
-#include "suggest/core/layout/proximity_info_utils.h"
-#include "suggest/core/policy/dictionary_structure_with_buffer_policy.h"
-
-namespace latinime {
-
-class DicNodeProximityFilter : public DictionaryStructureWithBufferPolicy::NodeFilter {
- public:
-    DicNodeProximityFilter(const ProximityInfoState *const pInfoState,
-            const int pointIndex, const bool exactOnly)
-            : mProximityInfoState(pInfoState), mPointIndex(pointIndex), mExactOnly(exactOnly) {}
-
-    bool isFilteredOut(const int codePoint) const {
-        return !isProximityCodePoint(codePoint);
-    }
-
- private:
-    DISALLOW_IMPLICIT_CONSTRUCTORS(DicNodeProximityFilter);
-
-    const ProximityInfoState *const mProximityInfoState;
-    const int mPointIndex;
-    const bool mExactOnly;
-
-    // TODO: Move to proximity info state
-    bool isProximityCodePoint(const int codePoint) const {
-        if (!mProximityInfoState) {
-            return true;
-        }
-        if (mExactOnly) {
-            return mProximityInfoState->getPrimaryCodePointAt(mPointIndex) == codePoint;
-        }
-        const ProximityType matchedId = mProximityInfoState->getProximityType(
-                mPointIndex, codePoint, true /* checkProximityChars */);
-        return ProximityInfoUtils::isMatchOrProximityChar(matchedId);
-    }
-};
-} // namespace latinime
-#endif // LATINIME_DIC_NODE_PROXIMITY_FILTER_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 150eb67..bb54e60 100644
--- a/native/jni/src/suggest/core/dicnode/dic_node_utils.cpp
+++ b/native/jni/src/suggest/core/dicnode/dic_node_utils.cpp
@@ -19,7 +19,6 @@
 #include <cstring>
 
 #include "suggest/core/dicnode/dic_node.h"
-#include "suggest/core/dicnode/dic_node_proximity_filter.h"
 #include "suggest/core/dicnode/dic_node_vector.h"
 #include "suggest/core/dictionary/multi_bigram_map.h"
 #include "suggest/core/dictionary/probability_utils.h"
@@ -52,38 +51,16 @@
 ///////////////////////////////////
 // Traverse node expansion utils //
 ///////////////////////////////////
-
-/* static */ void DicNodeUtils::createAndGetPassingChildNode(DicNode *dicNode,
-        const DicNodeProximityFilter *const childrenFilter,
-        DicNodeVector *childDicNodes) {
-    // Passing multiple chars node. No need to traverse child
-    const int codePoint = dicNode->getNodeTypedCodePoint();
-    const int baseLowerCaseCodePoint = CharUtils::toBaseLowerCase(codePoint);
-    if (!childrenFilter->isFilteredOut(codePoint)
-            || CharUtils::isIntentionalOmissionCodePoint(baseLowerCaseCodePoint)) {
-        childDicNodes->pushPassingChild(dicNode);
-    }
-}
-
 /* static */ void DicNodeUtils::getAllChildDicNodes(DicNode *dicNode,
         const DictionaryStructureWithBufferPolicy *const dictionaryStructurePolicy,
         DicNodeVector *childDicNodes) {
-    getProximityChildDicNodes(dicNode, dictionaryStructurePolicy, 0, 0, false, childDicNodes);
-}
-
-/* static */ void DicNodeUtils::getProximityChildDicNodes(DicNode *dicNode,
-        const DictionaryStructureWithBufferPolicy *const dictionaryStructurePolicy,
-        const ProximityInfoState *pInfoState, const int pointIndex, bool exactOnly,
-        DicNodeVector *childDicNodes) {
     if (dicNode->isTotalInputSizeExceedingLimit()) {
         return;
     }
-    const DicNodeProximityFilter childrenFilter(pInfoState, pointIndex, exactOnly);
     if (!dicNode->isLeavingNode()) {
-        DicNodeUtils::createAndGetPassingChildNode(dicNode, &childrenFilter, childDicNodes);
+        childDicNodes->pushPassingChild(dicNode);
     } else {
-        dictionaryStructurePolicy->createAndGetAllChildNodes(dicNode,
-                &childrenFilter, childDicNodes);
+        dictionaryStructurePolicy->createAndGetAllChildNodes(dicNode, childDicNodes);
     }
 }
 
diff --git a/native/jni/src/suggest/core/dicnode/dic_node_utils.h b/native/jni/src/suggest/core/dicnode/dic_node_utils.h
index 8dc984f..3fb351a 100644
--- a/native/jni/src/suggest/core/dicnode/dic_node_utils.h
+++ b/native/jni/src/suggest/core/dicnode/dic_node_utils.h
@@ -24,10 +24,8 @@
 namespace latinime {
 
 class DicNode;
-class DicNodeProximityFilter;
 class DicNodeVector;
 class DictionaryStructureWithBufferPolicy;
-class ProximityInfoState;
 class MultiBigramMap;
 
 class DicNodeUtils {
@@ -47,11 +45,6 @@
     static float getBigramNodeImprobability(
             const DictionaryStructureWithBufferPolicy *const dictionaryStructurePolicy,
             const DicNode *const node, MultiBigramMap *const multiBigramMap);
-    // TODO: Move to private
-    static void getProximityChildDicNodes(DicNode *dicNode,
-            const DictionaryStructureWithBufferPolicy *const dictionaryStructurePolicy,
-            const ProximityInfoState *pInfoState, const int pointIndex, bool exactOnly,
-            DicNodeVector *childDicNodes);
 
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(DicNodeUtils);
@@ -61,8 +54,6 @@
     static int getBigramNodeProbability(
             const DictionaryStructureWithBufferPolicy *const dictionaryStructurePolicy,
             const DicNode *const node, MultiBigramMap *multiBigramMap);
-    static void createAndGetPassingChildNode(DicNode *dicNode,
-            const DicNodeProximityFilter *const childrenFilter, DicNodeVector *childDicNodes);
 };
 } // namespace latinime
 #endif // LATINIME_DIC_NODE_UTILS_H
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 1cf4d67..5324115 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
@@ -33,25 +33,12 @@
  */
 class DictionaryStructureWithBufferPolicy {
  public:
-    // This provides a filtering method for filtering new node.
-    class NodeFilter {
-     public:
-        virtual bool isFilteredOut(const int codePoint) const = 0;
-
-     protected:
-        NodeFilter() {}
-        virtual ~NodeFilter() {}
-
-     private:
-        DISALLOW_COPY_AND_ASSIGN(NodeFilter);
-    };
-
     virtual ~DictionaryStructureWithBufferPolicy() {}
 
     virtual int getRootPosition() const = 0;
 
     virtual void createAndGetAllChildNodes(const DicNode *const dicNode,
-            const NodeFilter *const nodeFilter, DicNodeVector *const childDicNodes) const = 0;
+            DicNodeVector *const childDicNodes) const = 0;
 
     virtual int getCodePointsAndProbabilityAndReturnCodePointCount(
             const int nodePos, const int maxCodePointCount, int *const outCodePoints,
diff --git a/native/jni/src/suggest/core/suggest.cpp b/native/jni/src/suggest/core/suggest.cpp
index 3b77227..7d8dd21 100644
--- a/native/jni/src/suggest/core/suggest.cpp
+++ b/native/jni/src/suggest/core/suggest.cpp
@@ -456,7 +456,6 @@
         Weighting::addCostAndForwardInputIndex(WEIGHTING, CT_OMISSION, traverseSession,
                 dicNode, childDicNode, 0 /* multiBigramMap */);
         weightChildNode(traverseSession, childDicNode);
-
         if (!TRAVERSAL->isPossibleOmissionChildNode(traverseSession, dicNode, childDicNode)) {
             continue;
         }
@@ -472,11 +471,14 @@
         DicNode *dicNode) const {
     const int16_t pointIndex = dicNode->getInputIndex(0);
     DicNodeVector childDicNodes;
-    DicNodeUtils::getProximityChildDicNodes(dicNode,
-            traverseSession->getDictionaryStructurePolicy(),
-            traverseSession->getProximityInfoState(0), pointIndex + 1, true, &childDicNodes);
+    DicNodeUtils::getAllChildDicNodes(dicNode, traverseSession->getDictionaryStructurePolicy(),
+            &childDicNodes);
     const int size = childDicNodes.getSizeAndLock();
     for (int i = 0; i < size; i++) {
+        if (traverseSession->getProximityInfoState(0)->getPrimaryCodePointAt(pointIndex + 1)
+                != childDicNodes[i]->getNodeCodePoint()) {
+            continue;
+        }
         DicNode *const childDicNode = childDicNodes[i];
         Weighting::addCostAndForwardInputIndex(WEIGHTING, CT_INSERTION, traverseSession,
                 dicNode, childDicNode, 0 /* multiBigramMap */);
@@ -491,19 +493,29 @@
         DicNode *dicNode) const {
     const int16_t pointIndex = dicNode->getInputIndex(0);
     DicNodeVector childDicNodes1;
-    DicNodeUtils::getProximityChildDicNodes(dicNode,
-            traverseSession->getDictionaryStructurePolicy(),
-            traverseSession->getProximityInfoState(0), pointIndex + 1, false, &childDicNodes1);
+    DicNodeUtils::getAllChildDicNodes(dicNode, traverseSession->getDictionaryStructurePolicy(),
+            &childDicNodes1);
     const int childSize1 = childDicNodes1.getSizeAndLock();
     for (int i = 0; i < childSize1; i++) {
+        const ProximityType matchedId1 = traverseSession->getProximityInfoState(0)
+                ->getProximityType(pointIndex + 1, childDicNodes1[i]->getNodeCodePoint(),
+                        true /* checkProximityChars */);
+        if (!ProximityInfoUtils::isMatchOrProximityChar(matchedId1)) {
+            continue;
+        }
         if (childDicNodes1[i]->hasChildren()) {
             DicNodeVector childDicNodes2;
-            DicNodeUtils::getProximityChildDicNodes(
-                    childDicNodes1[i], traverseSession->getDictionaryStructurePolicy(),
-                    traverseSession->getProximityInfoState(0), pointIndex, false, &childDicNodes2);
+            DicNodeUtils::getAllChildDicNodes(childDicNodes1[i],
+                    traverseSession->getDictionaryStructurePolicy(), &childDicNodes2);
             const int childSize2 = childDicNodes2.getSizeAndLock();
             for (int j = 0; j < childSize2; j++) {
                 DicNode *const childDicNode2 = childDicNodes2[j];
+                const ProximityType matchedId2 = traverseSession->getProximityInfoState(0)
+                        ->getProximityType(pointIndex, childDicNode2->getNodeCodePoint(),
+                                true /* checkProximityChars */);
+                if (!ProximityInfoUtils::isMatchOrProximityChar(matchedId2)) {
+                    continue;
+                }
                 Weighting::addCostAndForwardInputIndex(WEIGHTING, CT_TRANSPOSITION,
                         traverseSession, childDicNodes1[i], childDicNode2, 0 /* multiBigramMap */);
                 processExpandedDicNode(traverseSession, childDicNode2);
diff --git a/native/jni/src/suggest/policyimpl/dictionary/bigram/bigram_list_reading_utils.cpp b/native/jni/src/suggest/policyimpl/dictionary/bigram/bigram_list_reading_utils.cpp
index 6f4fcbf..6da0e8b 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/bigram/bigram_list_reading_utils.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/bigram/bigram_list_reading_utils.cpp
@@ -16,7 +16,7 @@
 
 #include "suggest/policyimpl/dictionary/bigram/bigram_list_reading_utils.h"
 
-#include "suggest/core/dictionary/byte_array_utils.h"
+#include "suggest/policyimpl/dictionary/utils/byte_array_utils.h"
 
 namespace latinime {
 
diff --git a/native/jni/src/suggest/policyimpl/dictionary/bigram/bigram_list_reading_utils.h b/native/jni/src/suggest/policyimpl/dictionary/bigram/bigram_list_reading_utils.h
index 6b2bfe8..d0c584b 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/bigram/bigram_list_reading_utils.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/bigram/bigram_list_reading_utils.h
@@ -20,7 +20,7 @@
 #include <stdint.h>
 
 #include "defines.h"
-#include "suggest/core/dictionary/byte_array_utils.h"
+#include "suggest/policyimpl/dictionary/utils/byte_array_utils.h"
 
 namespace latinime {
 
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 3054e4e..34f092f 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
@@ -19,19 +19,19 @@
 #include <stdint.h>
 
 #include "defines.h"
-#include "suggest/core/dictionary/binary_dictionary_format_utils.h"
 #include "suggest/policyimpl/dictionary/dynamic_patricia_trie_policy.h"
 #include "suggest/policyimpl/dictionary/patricia_trie_policy.h"
+#include "suggest/policyimpl/dictionary/utils/format_utils.h"
 
 namespace latinime {
 
 /* static */ DictionaryStructureWithBufferPolicy *DictionaryStructureWithBufferPolicyFactory
         ::newDictionaryStructureWithBufferPolicy(const uint8_t *const dictBuf,
                 const int dictSize) {
-    switch (BinaryDictionaryFormatUtils::detectFormatVersion(dictBuf, dictSize)) {
-        case BinaryDictionaryFormatUtils::VERSION_2:
+    switch (FormatUtils::detectFormatVersion(dictBuf, dictSize)) {
+        case FormatUtils::VERSION_2:
             return new PatriciaTriePolicy(dictBuf);
-        case BinaryDictionaryFormatUtils::VERSION_3:
+        case FormatUtils::VERSION_3:
             return new DynamicPatriciaTriePolicy(dictBuf);
         default:
             ASSERT(false);
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 dbf3993..311e94d 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
@@ -29,7 +29,7 @@
 const int DynamicPatriciaTriePolicy::MAX_CHILD_COUNT_TO_AVOID_INFINITE_LOOP = 100000;
 
 void DynamicPatriciaTriePolicy::createAndGetAllChildNodes(const DicNode *const dicNode,
-        const NodeFilter *const nodeFilter, DicNodeVector *const childDicNodes) const {
+        DicNodeVector *const childDicNodes) const {
     if (!dicNode->hasChildren()) {
         return;
     }
@@ -52,8 +52,8 @@
         for (int i = 0; i < childCount; i++) {
             nodeReader.fetchNodeInfoFromBufferAndGetNodeCodePoints(nextPos, MAX_WORD_LENGTH,
                     mergedNodeCodePoints);
-            if (!nodeReader.isDeleted() && !nodeFilter->isFilteredOut(mergedNodeCodePoints[0])) {
-                // Push child node when the node is not deleted and not filtered out.
+            if (!nodeReader.isDeleted()) {
+                // Push child node when the node is not a deleted node.
                 childDicNodes->pushLeavingChild(dicNode, nodeReader.getNodePos(),
                         nodeReader.getChildrenPos(), nodeReader.getProbability(),
                         nodeReader.isTerminal(), nodeReader.hasChildren(),
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 bd254ab..4a5192c 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
@@ -43,7 +43,7 @@
     }
 
     void createAndGetAllChildNodes(const DicNode *const dicNode,
-            const NodeFilter *const nodeFilter, DicNodeVector *const childDicNodes) const;
+            DicNodeVector *const childDicNodes) const;
 
     int getCodePointsAndProbabilityAndReturnCodePointCount(
             const int terminalNodePos, const int maxCodePointCount, int *const outCodePoints,
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 0de6341..1ef3b65 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
@@ -17,7 +17,7 @@
 #include "suggest/policyimpl/dictionary/dynamic_patricia_trie_reading_utils.h"
 
 #include "defines.h"
-#include "suggest/core/dictionary/byte_array_utils.h"
+#include "suggest/policyimpl/dictionary/utils/byte_array_utils.h"
 
 namespace latinime {
 
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 5398d7e..a6cb46d 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,7 @@
 #include <stdint.h>
 
 #include "defines.h"
-#include "suggest/core/dictionary/byte_array_utils.h"
+#include "suggest/policyimpl/dictionary/utils/byte_array_utils.h"
 
 namespace latinime {
 
diff --git a/native/jni/src/suggest/policyimpl/dictionary/header/header_reading_utils.cpp b/native/jni/src/suggest/policyimpl/dictionary/header/header_reading_utils.cpp
index 70f45df..f323876 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/header/header_reading_utils.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/header/header_reading_utils.cpp
@@ -20,7 +20,7 @@
 #include <cstdlib>
 
 #include "defines.h"
-#include "suggest/core/dictionary/byte_array_utils.h"
+#include "suggest/policyimpl/dictionary/utils/byte_array_utils.h"
 
 namespace latinime {
 
@@ -34,7 +34,7 @@
 const HeaderReadingUtils::DictionaryFlags
         HeaderReadingUtils::NO_FLAGS = 0;
 // Flags for special processing
-// Those *must* match the flags in makedict (BinaryDictInputOutput#*_PROCESSING_FLAG) or
+// Those *must* match the flags in makedict (FormatSpec#*_PROCESSING_FLAG) or
 // something very bad (like, the apocalypse) will happen. Please update both at the same time.
 const HeaderReadingUtils::DictionaryFlags
         HeaderReadingUtils::GERMAN_UMLAUT_PROCESSING_FLAG = 0x1;
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 8ce2b3e..3e664a2 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/patricia_trie_policy.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/patricia_trie_policy.cpp
@@ -26,7 +26,7 @@
 namespace latinime {
 
 void PatriciaTriePolicy::createAndGetAllChildNodes(const DicNode *const dicNode,
-        const NodeFilter *const nodeFilter, DicNodeVector *const childDicNodes) const {
+        DicNodeVector *const childDicNodes) const {
     if (!dicNode->hasChildren()) {
         return;
     }
@@ -34,7 +34,7 @@
     const int childCount = PatriciaTrieReadingUtils::getGroupCountAndAdvancePosition(
             mDictRoot, &nextPos);
     for (int i = 0; i < childCount; i++) {
-        nextPos = createAndGetLeavingChildNode(dicNode, nextPos, nodeFilter, childDicNodes);
+        nextPos = createAndGetLeavingChildNode(dicNode, nextPos, childDicNodes);
     }
 }
 
@@ -116,8 +116,7 @@
 }
 
 int PatriciaTriePolicy::createAndGetLeavingChildNode(const DicNode *const dicNode,
-        const int nodePos,  const NodeFilter *const childrenFilter,
-        DicNodeVector *childDicNodes) const {
+        const int nodePos, DicNodeVector *childDicNodes) const {
     int pos = nodePos;
     const PatriciaTrieReadingUtils::NodeFlags flags =
             PatriciaTrieReadingUtils::getFlagsAndAdvancePosition(mDictRoot, &pos);
@@ -136,14 +135,12 @@
     if (PatriciaTrieReadingUtils::hasBigrams(flags)) {
         getBigramsStructurePolicy()->skipAllBigrams(&pos);
     }
-    if (!childrenFilter->isFilteredOut(mergedNodeCodePoints[0])) {
-        childDicNodes->pushLeavingChild(dicNode, nodePos, childrenPos, probability,
-                PatriciaTrieReadingUtils::isTerminal(flags),
-                PatriciaTrieReadingUtils::hasChildrenInFlags(flags),
-                PatriciaTrieReadingUtils::isBlacklisted(flags) ||
-                        PatriciaTrieReadingUtils::isNotAWord(flags),
-                mergedNodeCodePointCount, mergedNodeCodePoints);
-    }
+    childDicNodes->pushLeavingChild(dicNode, nodePos, childrenPos, probability,
+            PatriciaTrieReadingUtils::isTerminal(flags),
+            PatriciaTrieReadingUtils::hasChildrenInFlags(flags),
+            PatriciaTrieReadingUtils::isBlacklisted(flags) ||
+                    PatriciaTrieReadingUtils::isNotAWord(flags),
+            mergedNodeCodePointCount, mergedNodeCodePoints);
     return pos;
 }
 
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 96fdd4c..0ed7945 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/patricia_trie_policy.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/patricia_trie_policy.h
@@ -43,7 +43,7 @@
     }
 
     void createAndGetAllChildNodes(const DicNode *const dicNode,
-            const NodeFilter *const nodeFilter, DicNodeVector *const childDicNodes) const;
+            DicNodeVector *const childDicNodes) const;
 
     int getCodePointsAndProbabilityAndReturnCodePointCount(
             const int terminalNodePos, const int maxCodePointCount, int *const outCodePoints,
@@ -96,7 +96,7 @@
     const ShortcutListPolicy mShortcutListPolicy;
 
     int createAndGetLeavingChildNode(const DicNode *const dicNode, const int nodePos,
-            const NodeFilter *const nodeFilter, DicNodeVector *const childDicNodes) const;
+            DicNodeVector *const childDicNodes) const;
 };
 } // namespace latinime
 #endif // LATINIME_PATRICIA_TRIE_POLICY_H
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 89e981d..003b943 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
@@ -17,7 +17,7 @@
 #include "suggest/policyimpl/dictionary/patricia_trie_reading_utils.h"
 
 #include "defines.h"
-#include "suggest/core/dictionary/byte_array_utils.h"
+#include "suggest/policyimpl/dictionary/utils/byte_array_utils.h"
 
 namespace latinime {
 
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 002c3f1..9f2fc20 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,7 @@
 #include <stdint.h>
 
 #include "defines.h"
-#include "suggest/core/dictionary/byte_array_utils.h"
+#include "suggest/policyimpl/dictionary/utils/byte_array_utils.h"
 
 namespace latinime {
 
diff --git a/native/jni/src/suggest/policyimpl/dictionary/shortcut/shortcut_list_policy.h b/native/jni/src/suggest/policyimpl/dictionary/shortcut/shortcut_list_policy.h
index 3c7fab0..d73f739 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/shortcut/shortcut_list_policy.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/shortcut/shortcut_list_policy.h
@@ -33,6 +33,9 @@
     ~ShortcutListPolicy() {}
 
     int getStartPos(const int pos) const {
+        if (pos == NOT_A_DICT_POS) {
+            return NOT_A_DICT_POS;
+        }
         int listPos = pos;
         ShortcutListReadingUtils::getShortcutListSizeAndForwardPointer(mShortcutsBuf, &listPos);
         return listPos;
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 e92fa5f..b5bb964 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,7 @@
 #include <stdint.h>
 
 #include "defines.h"
-#include "suggest/core/dictionary/byte_array_utils.h"
+#include "suggest/policyimpl/dictionary/utils/byte_array_utils.h"
 
 namespace latinime {
 
diff --git a/native/jni/src/suggest/core/dictionary/byte_array_utils.cpp b/native/jni/src/suggest/policyimpl/dictionary/utils/byte_array_utils.cpp
similarity index 92%
rename from native/jni/src/suggest/core/dictionary/byte_array_utils.cpp
rename to native/jni/src/suggest/policyimpl/dictionary/utils/byte_array_utils.cpp
index 68b1d5d..a84cfb9 100644
--- a/native/jni/src/suggest/core/dictionary/byte_array_utils.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/utils/byte_array_utils.cpp
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#include "suggest/core/dictionary/byte_array_utils.h"
+#include "suggest/policyimpl/dictionary/utils/byte_array_utils.h"
 
 namespace latinime {
 
diff --git a/native/jni/src/suggest/core/dictionary/byte_array_utils.h b/native/jni/src/suggest/policyimpl/dictionary/utils/byte_array_utils.h
similarity index 100%
rename from native/jni/src/suggest/core/dictionary/byte_array_utils.h
rename to native/jni/src/suggest/policyimpl/dictionary/utils/byte_array_utils.h
diff --git a/native/jni/src/suggest/core/dictionary/binary_dictionary_format_utils.cpp b/native/jni/src/suggest/policyimpl/dictionary/utils/format_utils.cpp
similarity index 73%
rename from native/jni/src/suggest/core/dictionary/binary_dictionary_format_utils.cpp
rename to native/jni/src/suggest/policyimpl/dictionary/utils/format_utils.cpp
index 15f2aa6..3796c7b 100644
--- a/native/jni/src/suggest/core/dictionary/binary_dictionary_format_utils.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/utils/format_utils.cpp
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
-#include "suggest/core/dictionary/binary_dictionary_format_utils.h"
+#include "suggest/policyimpl/dictionary/utils/format_utils.h"
 
-#include "suggest/core/dictionary/byte_array_utils.h"
+#include "suggest/policyimpl/dictionary/utils/byte_array_utils.h"
 
 namespace latinime {
 
@@ -24,21 +24,19 @@
  * Dictionary size
  */
 // Any file smaller than this is not a dictionary.
-const int BinaryDictionaryFormatUtils::DICTIONARY_MINIMUM_SIZE = 4;
+const int FormatUtils::DICTIONARY_MINIMUM_SIZE = 4;
 
 /**
  * Format versions
  */
-// The versions of Latin IME that only handle format version 1 only test for the magic
-// number, so we had to change it so that version 2 files would be rejected by older
-// implementations. On this occasion, we made the magic number 32 bits long.
-const uint32_t BinaryDictionaryFormatUtils::HEADER_VERSION_2_MAGIC_NUMBER = 0x9BC13AFE;
+// 32 bit magic number is stored at the beginning of the dictionary header to reject unsupported
+// or obsolete dictionary formats.
+const uint32_t FormatUtils::HEADER_VERSION_2_MAGIC_NUMBER = 0x9BC13AFE;
 // Magic number (4 bytes), version (2 bytes), options (2 bytes), header size (4 bytes) = 12
-const int BinaryDictionaryFormatUtils::HEADER_VERSION_2_MINIMUM_SIZE = 12;
+const int FormatUtils::HEADER_VERSION_2_MINIMUM_SIZE = 12;
 
-/* static */ BinaryDictionaryFormatUtils::FORMAT_VERSION
-        BinaryDictionaryFormatUtils::detectFormatVersion(const uint8_t *const dict,
-                const int dictSize) {
+/* static */ FormatUtils::FORMAT_VERSION FormatUtils::detectFormatVersion(
+        const uint8_t *const dict, const int dictSize) {
     // The magic number is stored big-endian.
     // If the dictionary is less than 4 bytes, we can't even read the magic number, so we don't
     // understand this format.
diff --git a/native/jni/src/suggest/core/dictionary/binary_dictionary_format_utils.h b/native/jni/src/suggest/policyimpl/dictionary/utils/format_utils.h
similarity index 69%
rename from native/jni/src/suggest/core/dictionary/binary_dictionary_format_utils.h
rename to native/jni/src/suggest/policyimpl/dictionary/utils/format_utils.h
index 62c7376..f843215 100644
--- a/native/jni/src/suggest/core/dictionary/binary_dictionary_format_utils.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/utils/format_utils.h
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
-#ifndef LATINIME_BINARY_DICTIONARY_FORMAT_UTILS_H
-#define LATINIME_BINARY_DICTIONARY_FORMAT_UTILS_H
+#ifndef LATINIME_FORMAT_UTILS_H
+#define LATINIME_FORMAT_UTILS_H
 
 #include <stdint.h>
 
@@ -25,12 +25,8 @@
 
 /**
  * Methods to handle binary dictionary format version.
- *
- * Currently, we have a file with a similar name, binary_format.h. binary_format.h contains binary
- * reading methods and utility methods for various purposes.
- * On the other hand, this file deals with only about dictionary format version.
  */
-class BinaryDictionaryFormatUtils {
+class FormatUtils {
  public:
     enum FORMAT_VERSION {
         VERSION_2,
@@ -41,11 +37,11 @@
     static FORMAT_VERSION detectFormatVersion(const uint8_t *const dict, const int dictSize);
 
  private:
-    DISALLOW_IMPLICIT_CONSTRUCTORS(BinaryDictionaryFormatUtils);
+    DISALLOW_IMPLICIT_CONSTRUCTORS(FormatUtils);
 
     static const int DICTIONARY_MINIMUM_SIZE;
     static const uint32_t HEADER_VERSION_2_MAGIC_NUMBER;
     static const int HEADER_VERSION_2_MINIMUM_SIZE;
 };
 } // namespace latinime
-#endif /* LATINIME_BINARY_DICTIONARY_FORMAT_UTILS_H */
+#endif /* LATINIME_FORMAT_UTILS_H */
diff --git a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOTests.java b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOTests.java
index e4b5ad2..7bfd603 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOTests.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOTests.java
@@ -619,13 +619,13 @@
         try {
             MoreAsserts.assertNotEqual(FormatSpec.NOT_VALID_WORD,
                     BinaryDictIOUtils.getTerminalPosition(buffer, sWords.get(0)));
-            BinaryDictIOUtils.deleteWord(buffer, sWords.get(0));
+            DynamicBinaryDictIOUtils.deleteWord(buffer, sWords.get(0));
             assertEquals(FormatSpec.NOT_VALID_WORD,
                     BinaryDictIOUtils.getTerminalPosition(buffer, sWords.get(0)));
 
             MoreAsserts.assertNotEqual(FormatSpec.NOT_VALID_WORD,
                     BinaryDictIOUtils.getTerminalPosition(buffer, sWords.get(5)));
-            BinaryDictIOUtils.deleteWord(buffer, sWords.get(5));
+            DynamicBinaryDictIOUtils.deleteWord(buffer, sWords.get(5));
             assertEquals(FormatSpec.NOT_VALID_WORD,
                     BinaryDictIOUtils.getTerminalPosition(buffer, sWords.get(5)));
         } catch (IOException e) {
diff --git a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtilsTests.java b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtilsTests.java
index 9331da4..e759507 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtilsTests.java
@@ -197,8 +197,8 @@
                 assertEquals(FormatSpec.NOT_VALID_WORD, getWordPosition(file, word));
             }
             final long now = System.nanoTime();
-            BinaryDictIOUtils.insertWord(buffer, outStream, word, frequency, bigrams, shortcuts,
-                    false, false);
+            DynamicBinaryDictIOUtils.insertWord(buffer, outStream, word, frequency, bigrams,
+                    shortcuts, false, false);
             amountOfTime = System.nanoTime() - now;
             outStream.flush();
             MoreAsserts.assertNotEqual(FormatSpec.NOT_VALID_WORD, getWordPosition(file, word));
@@ -232,7 +232,7 @@
             raFile = new RandomAccessFile(file, "rw");
             buffer = new ByteBufferWrapper(raFile.getChannel().map(
                     FileChannel.MapMode.READ_WRITE, 0, file.length()));
-            BinaryDictIOUtils.deleteWord(buffer, word);
+            DynamicBinaryDictIOUtils.deleteWord(buffer, word);
         } catch (IOException e) {
         } catch (UnsupportedFormatException e) {
         } finally {
diff --git a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictReaderTests.java b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictReaderTests.java
index 5f6950a..a46e583 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictReaderTests.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictReaderTests.java
@@ -18,8 +18,12 @@
 
 import com.android.inputmethod.latin.makedict.BinaryDictInputOutput.FusionDictionaryBufferInterface;
 import com.android.inputmethod.latin.makedict.BinaryDictReader.FusionDictionaryBufferFactory;
-import com.android.inputmethod.latin.makedict.BinaryDictReader.FusionDictionaryBufferFromByteArrayFactory;
-import com.android.inputmethod.latin.makedict.BinaryDictReader.FusionDictionaryBufferFromByteBufferFactory;
+import com.android.inputmethod.latin.makedict.BinaryDictReader.
+        FusionDictionaryBufferFromByteArrayFactory;
+import com.android.inputmethod.latin.makedict.BinaryDictReader.
+        FusionDictionaryBufferFromByteBufferFactory;
+import com.android.inputmethod.latin.makedict.BinaryDictReader.
+        FusionDictionaryBufferFromWritableByteBufferFactory;
 
 import android.test.AndroidTestCase;
 import android.util.Log;
@@ -94,6 +98,11 @@
                 new FusionDictionaryBufferFromByteArrayFactory());
     }
 
+    public void testOpenBufferWithWritableByteBuffer() {
+        runTestOpenBuffer("testOpenBufferWithWritableByteBuffer",
+                new FusionDictionaryBufferFromWritableByteBufferFactory());
+    }
+
     @SuppressWarnings("null")
     public void runTestGetBuffer(final String testName,
             final FusionDictionaryBufferFactory factory) {
@@ -135,4 +144,9 @@
         runTestGetBuffer("testGetBufferWithByteArray",
                 new FusionDictionaryBufferFromByteArrayFactory());
     }
+
+    public void testGetBufferWithWritableByteBuffer() {
+        runTestGetBuffer("testGetBufferWithWritableByteBuffer",
+                new FusionDictionaryBufferFromWritableByteBufferFactory());
+    }
 }
diff --git a/tests/src/com/android/inputmethod/latin/utils/StringUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/StringUtilsTests.java
index 9ee8e38..175e511 100644
--- a/tests/src/com/android/inputmethod/latin/utils/StringUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/StringUtilsTests.java
@@ -16,6 +16,8 @@
 
 package com.android.inputmethod.latin.utils;
 
+import com.android.inputmethod.latin.settings.SettingsValues;
+
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 
@@ -183,6 +185,18 @@
         assertTrue(StringUtils.isIdenticalAfterDowncase(""));
     }
 
+    public void testLooksValidForDictionaryInsertion() {
+        final SettingsValues settings =
+                SettingsValues.makeDummySettingsValuesForTest(Locale.ENGLISH);
+        assertTrue(StringUtils.looksValidForDictionaryInsertion("aochaueo", settings));
+        assertFalse(StringUtils.looksValidForDictionaryInsertion("", settings));
+        assertTrue(StringUtils.looksValidForDictionaryInsertion("ao-ch'aueo", settings));
+        assertFalse(StringUtils.looksValidForDictionaryInsertion("2908743256", settings));
+        assertTrue(StringUtils.looksValidForDictionaryInsertion("31aochaueo", settings));
+        assertFalse(StringUtils.looksValidForDictionaryInsertion("akeo  raeoch oerch .", settings));
+        assertFalse(StringUtils.looksValidForDictionaryInsertion("!!!", settings));
+    }
+
     private static void checkCapitalize(final String src, final String dst, final String separators,
             final Locale locale) {
         assertEquals(dst, StringUtils.capitalizeEachWord(src, separators, locale));