diff --git a/tools/makedict2/Android.mk b/tools/makedict2/Android.mk
new file mode 100644
index 0000000000000000000000000000000000000000..e056168e975d46a3a687e9a7fc22dff7274a2374
--- /dev/null
+++ b/tools/makedict2/Android.mk
@@ -0,0 +1,27 @@
+#
+# Copyright (C) 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.
+#
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under,src)
+LOCAL_SRC_FILES += $(call all-java-files-under,tests)
+LOCAL_JAR_MANIFEST := etc/manifest.txt
+LOCAL_MODULE_TAGS := eng
+LOCAL_MODULE := makedict2
+LOCAL_JAVA_LIBRARIES := junit
+
+include $(BUILD_HOST_JAVA_LIBRARY)
+include $(LOCAL_PATH)/etc/Android.mk
diff --git a/tools/makedict2/etc/Android.mk b/tools/makedict2/etc/Android.mk
new file mode 100644
index 0000000000000000000000000000000000000000..c71377cdf3d42493fc4c8da1a715506f275ee4da
--- /dev/null
+++ b/tools/makedict2/etc/Android.mk
@@ -0,0 +1,21 @@
+# Copyright (C) 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := eng
+
+LOCAL_PREBUILT_EXECUTABLES := makedict2
+include $(BUILD_HOST_PREBUILT)
diff --git a/tools/makedict2/etc/makedict2 b/tools/makedict2/etc/makedict2
new file mode 100755
index 0000000000000000000000000000000000000000..1bad825e1e4ca0d82c9ec8efe1e1881b79acae76
--- /dev/null
+++ b/tools/makedict2/etc/makedict2
@@ -0,0 +1,63 @@
+#!/bin/sh
+# 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.
+
+# Set up prog to be the path of this script, including following symlinks,
+# and set up progdir to be the fully-qualified pathname of its directory.
+prog="$0"
+while [ -h "${prog}" ]; do
+    newProg=`/bin/ls -ld "${prog}"`
+    newProg=`expr "${newProg}" : ".* -> \(.*\)$"`
+    if expr "x${newProg}" : 'x/' >/dev/null; then
+        prog="${newProg}"
+    else
+        progdir=`dirname "${prog}"`
+        prog="${progdir}/${newProg}"
+    fi
+done
+oldwd=`pwd`
+progdir=`dirname "${prog}"`
+cd "${progdir}"
+progdir=`pwd`
+prog="${progdir}"/`basename "${prog}"`
+cd "${oldwd}"
+
+jarfile=makedict2.jar
+frameworkdir="$progdir"
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+    frameworkdir=`dirname "$progdir"`/tools/lib
+    libdir=`dirname "$progdir"`/tools/lib
+fi
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+    frameworkdir=`dirname "$progdir"`/framework
+    libdir=`dirname "$progdir"`/lib
+fi
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+    echo `basename "$prog"`": can't find $jarfile"
+    exit 1
+fi
+
+if [ "$OSTYPE" = "cygwin" ] ; then
+    jarpath=`cygpath -w  "$frameworkdir/$jarfile"`
+    progdir=`cygpath -w  "$progdir"`
+else
+    jarpath="$frameworkdir/$jarfile"
+fi
+
+# need to use "java.ext.dirs" because "-jar" causes classpath to be ignored
+# might need more memory, e.g. -Xmx128M
+exec java -ea -Djava.ext.dirs="$frameworkdir" -jar "$jarpath" "$@"
diff --git a/tools/makedict2/etc/manifest.txt b/tools/makedict2/etc/manifest.txt
new file mode 100644
index 0000000000000000000000000000000000000000..948609da6fb41ef6de8c29a5be2e1acbc751e96c
--- /dev/null
+++ b/tools/makedict2/etc/manifest.txt
@@ -0,0 +1 @@
+Main-Class: com.android.inputmethod.latin.DictionaryMaker
diff --git a/tools/makedict2/src/com/android/inputmethod/latin/BinaryDictInputOutput.java b/tools/makedict2/src/com/android/inputmethod/latin/BinaryDictInputOutput.java
new file mode 100644
index 0000000000000000000000000000000000000000..92f402d3e8b3ff592a03426388c173a1e48605fa
--- /dev/null
+++ b/tools/makedict2/src/com/android/inputmethod/latin/BinaryDictInputOutput.java
@@ -0,0 +1,1024 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package com.android.inputmethod.latin;
+
+import com.android.inputmethod.latin.FusionDictionary.CharGroup;
+import com.android.inputmethod.latin.FusionDictionary.Node;
+import com.android.inputmethod.latin.FusionDictionary.WeightedString;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Reads and writes XML files for a FusionDictionary.
+ *
+ * All the methods in this class are static.
+ */
+public class BinaryDictInputOutput {
+
+    /* Node layout is as follows:
+     *   | addressType                         xx     : mask with MASK_GROUP_ADDRESS_TYPE
+     *                                 2 bits, 00 = no children : FLAG_GROUP_ADDRESS_TYPE_NOADDRESS
+     * f |                                     01 = 1 byte      : FLAG_GROUP_ADDRESS_TYPE_ONEBYTE
+     * l |                                     10 = 2 bytes     : FLAG_GROUP_ADDRESS_TYPE_TWOBYTES
+     * a |                                     11 = 3 bytes     : FLAG_GROUP_ADDRESS_TYPE_THREEBYTES
+     * g | has several chars ?         1 bit, 1 = yes, 0 = no   : FLAG_HAS_MULTIPLE_CHARS
+     * s | has a terminal ?            1 bit, 1 = yes, 0 = no   : FLAG_IS_TERMINAL
+     *   | reserved                    1 bit, 1 = yes, 0 = no
+     *   | has bigrams ?               1 bit, 1 = yes, 0 = no   : FLAG_HAS_BIGRAMS
+     *
+     * c | IF FLAG_HAS_MULTIPLE_CHARS
+     * h |   char, char, char, char    n * (1 or 3 bytes) : use CharGroupInfo for i/o helpers
+     * a |   end                       1 byte, = 0
+     * r | ELSE
+     * s |   char                      1 or 3 bytes
+     *   | END
+     *
+     * f |
+     * r | IF FLAG_IS_TERMINAL
+     * e |   frequency                 1 byte
+     * q |
+     *
+     * c | IF 00 = FLAG_GROUP_ADDRESS_TYPE_NOADDRESS = addressType
+     * h |   // nothing
+     * i | ELSIF 01 = FLAG_GROUP_ADDRESS_TYPE_ONEBYTE == addressType
+     * l |   children address, 1 byte
+     * d | ELSIF 10 = FLAG_GROUP_ADDRESS_TYPE_TWOBYTES == addressType
+     * r |   children address, 2 bytes
+     * e | ELSE // 11 = FLAG_GROUP_ADDRESS_TYPE_THREEBYTES = addressType
+     * n |   children address, 3 bytes
+     * A | END
+     * d
+     * dress
+     *
+     *   | IF FLAG_IS_TERMINAL && FLAG_HAS_BIGRAMS
+     *   | bigrams address list
+     *
+     * Char format is:
+     * 1 byte = bbbbbbbb match
+     * case 000xxxxx: xxxxx << 16 + next byte << 8 + next byte
+     * else: if 00011111 (= 0x1F) : this is the terminator. This is a relevant choice because
+     *       unicode code points range from 0 to 0x10FFFF, so any 3-byte value starting with
+     *       00011111 would be outside unicode.
+     * else: iso-latin-1 code
+     * This allows for the whole unicode range to be encoded, including chars outside of
+     * the BMP. Also everything in the iso-latin-1 charset is only 1 byte, except control
+     * characters which should never happen anyway (and still work, but take 3 bytes).
+     *
+     * bigram and shortcut address list is:
+     * <flags> = | hasNext = 1 bit, 1 = yes, 0 = no     : FLAG_ATTRIBUTE_HAS_NEXT
+     *           | addressSign = 1 bit,                 : FLAG_ATTRIBUTE_OFFSET_NEGATIVE
+     *           |                      1 = must take -address, 0 = must take +address
+     *           |                         xx : mask with MASK_ATTRIBUTE_ADDRESS_TYPE
+     *           | addressFormat = 2 bits, 00 = unused  : FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE
+     *           |                         01 = 1 byte  : FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE
+     *           |                         10 = 2 bytes : FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES
+     *           |                         11 = 3 bytes : FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES
+     *           | 4 bits : frequency         : mask with FLAG_ATTRIBUTE_FREQUENCY
+     * <address> | IF (01 == FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE == addressFormat)
+     *           |   read 1 byte, add top 4 bits
+     *           | ELSIF (10 == FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES == addressFormat)
+     *           |   read 2 bytes, add top 4 bits
+     *           | ELSE // 11 == FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES == addressFormat
+     *           |   read 3 bytes, add top 4 bits
+     *           | END
+     *           | if (FLAG_ATTRIBUTE_OFFSET_NEGATIVE) then address = -address
+     * if (FLAG_ATTRIBUTE_HAS_NET) goto bigram_and_shortcut_address_list_is
+     *
+     */
+
+    private static final int MAGIC_NUMBER = 0x78B1;
+    private static final int VERSION = 1;
+    private static final int MAXIMUM_SUPPORTED_VERSION = VERSION;
+    // No options yet, reserved for future use.
+    private static final int OPTIONS = 0;
+
+    // TODO: Make this value adaptative to content data, store it in the header, and
+    // use it in the reading code.
+    private static final int MAX_WORD_LENGTH = 48;
+
+    private static final int MASK_GROUP_ADDRESS_TYPE = 0xC0;
+    private static final int FLAG_GROUP_ADDRESS_TYPE_NOADDRESS = 0x00;
+    private static final int FLAG_GROUP_ADDRESS_TYPE_ONEBYTE = 0x40;
+    private static final int FLAG_GROUP_ADDRESS_TYPE_TWOBYTES = 0x80;
+    private static final int FLAG_GROUP_ADDRESS_TYPE_THREEBYTES = 0xC0;
+
+    private static final int FLAG_HAS_MULTIPLE_CHARS = 0x20;
+
+    private static final int FLAG_IS_TERMINAL = 0x10;
+    private static final int FLAG_HAS_BIGRAMS = 0x04;
+
+    private static final int FLAG_ATTRIBUTE_HAS_NEXT = 0x80;
+    private static final int FLAG_ATTRIBUTE_OFFSET_NEGATIVE = 0x40;
+    private static final int MASK_ATTRIBUTE_ADDRESS_TYPE = 0x30;
+    private static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE = 0x10;
+    private static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES = 0x20;
+    private static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES = 0x30;
+    private static final int FLAG_ATTRIBUTE_FREQUENCY = 0x0F;
+
+    private static final int GROUP_CHARACTERS_TERMINATOR = 0x1F;
+
+    private static final int GROUP_COUNT_SIZE = 1;
+    private static final int GROUP_TERMINATOR_SIZE = 1;
+    private static final int GROUP_FLAGS_SIZE = 1;
+    private static final int GROUP_FREQUENCY_SIZE = 1;
+    private static final int GROUP_MAX_ADDRESS_SIZE = 3;
+    private static final int GROUP_ATTRIBUTE_FLAGS_SIZE = 1;
+    private static final int GROUP_ATTRIBUTE_MAX_ADDRESS_SIZE = 3;
+
+    private static final int NO_CHILDREN_ADDRESS = Integer.MIN_VALUE;
+    private static final int INVALID_CHARACTER = -1;
+
+    // Limiting to 127 for upward compatibility
+    // TODO: implement a scheme to be able to shoot 256 chargroups in a node
+    private static final int MAX_CHARGROUPS_IN_A_NODE = 127;
+
+    private static final int MAX_TERMINAL_FREQUENCY = 255;
+
+    /**
+     * A class grouping utility function for our specific character encoding.
+     */
+    private static class CharEncoding {
+
+        private static final int MINIMAL_ONE_BYTE_CHARACTER_VALUE = 0x20;
+        private static final int MAXIMAL_ONE_BYTE_CHARACTER_VALUE = 0xFF;
+
+        /**
+         * Helper method to find out whether this code fits on one byte
+         */
+        private static boolean fitsOnOneByte(int character) {
+            return character >= MINIMAL_ONE_BYTE_CHARACTER_VALUE
+                    && character <= MAXIMAL_ONE_BYTE_CHARACTER_VALUE;
+        }
+
+        /**
+         * Compute the size of a character given its character code.
+         *
+         * Char format is:
+         * 1 byte = bbbbbbbb match
+         * case 000xxxxx: xxxxx << 16 + next byte << 8 + next byte
+         * else: if 00011111 (= 0x1F) : this is the terminator. This is a relevant choice because
+         *       unicode code points range from 0 to 0x10FFFF, so any 3-byte value starting with
+         *       00011111 would be outside unicode.
+         * else: iso-latin-1 code
+         * This allows for the whole unicode range to be encoded, including chars outside of
+         * the BMP. Also everything in the iso-latin-1 charset is only 1 byte, except control
+         * characters which should never happen anyway (and still work, but take 3 bytes).
+         *
+         * @param character the character code.
+         * @return the size in binary encoded-form, either 1 or 3 bytes.
+         */
+        private static int getCharSize(int character) {
+            // See char encoding in FusionDictionary.java
+            if (fitsOnOneByte(character)) return 1;
+            if (INVALID_CHARACTER == character) return 1;
+            return 3;
+        }
+
+        /**
+         * Compute the byte size of a character array.
+         */
+        private static int getCharArraySize(final int[] chars) {
+            int size = 0;
+            for (int character : chars) size += getCharSize(character);
+            return size;
+        }
+
+        /**
+         * Writes a char array to a byte buffer.
+         *
+         * @param characters the character array to write.
+         * @param buffer the byte buffer to write to.
+         * @param index the index in buffer to write the character array to.
+         * @return the index after the last character.
+         */
+        private static int writeCharArray(int[] characters, byte[] buffer, int index) {
+            for (int character : characters) {
+                if (1 == getCharSize(character)) {
+                    buffer[index++] = (byte)character;
+                } else {
+                    buffer[index++] = (byte)(0xFF & (character >> 16));
+                    buffer[index++] = (byte)(0xFF & (character >> 8));
+                    buffer[index++] = (byte)(0xFF & character);
+                }
+            }
+            return index;
+        }
+
+        /**
+         * Reads a character from the file.
+         *
+         * This follows the character format documented earlier in this source file.
+         *
+         * @param source the file, positioned over an encoded character.
+         * @return the character code.
+         */
+        private static int readChar(RandomAccessFile source) throws IOException {
+            int character = source.readUnsignedByte();
+            if (!fitsOnOneByte(character)) {
+                if (GROUP_CHARACTERS_TERMINATOR == character)
+                    return INVALID_CHARACTER;
+                character <<= 16;
+                character += source.readUnsignedShort();
+            }
+            return character;
+        }
+    }
+
+    /**
+     * Compute the binary size of the character array in a group
+     *
+     * If only one character, this is the size of this character. If many, it's the sum of their
+     * sizes + 1 byte for the terminator.
+     *
+     * @param group the group
+     * @return the size of the char array, including the terminator if any
+     */
+    private static int getGroupCharactersSize(CharGroup group) {
+        int size = CharEncoding.getCharArraySize(group.mChars);
+        if (group.hasSeveralChars()) size += GROUP_TERMINATOR_SIZE;
+        return size;
+    }
+
+    /**
+     * Compute the maximum size of a CharGroup, assuming 3-byte addresses for everything.
+     *
+     * @param group the CharGroup to compute the size of.
+     * @return the maximum size of the group.
+     */
+    private static int getCharGroupMaximumSize(CharGroup group) {
+        int size = getGroupCharactersSize(group) + GROUP_FLAGS_SIZE;
+        // If terminal, one byte for the frequency
+        if (group.isTerminal()) size += GROUP_FREQUENCY_SIZE;
+        size += GROUP_MAX_ADDRESS_SIZE; // For children address
+        if (null != group.mBigrams) {
+            for (WeightedString bigram : group.mBigrams) {
+                size += GROUP_ATTRIBUTE_FLAGS_SIZE + GROUP_ATTRIBUTE_MAX_ADDRESS_SIZE;
+            }
+        }
+        return size;
+    }
+
+    /**
+     * Compute the maximum size of a node, assuming 3-byte addresses for everything, and caches
+     * it in the 'actualSize' member of the node.
+     *
+     * @param node the node to compute the maximum size of.
+     */
+    private static void setNodeMaximumSize(Node node) {
+        int size = GROUP_COUNT_SIZE;
+        for (CharGroup g : node.mData) {
+            final int groupSize = getCharGroupMaximumSize(g);
+            g.mCachedSize = groupSize;
+            size += groupSize;
+        }
+        node.mCachedSize = size;
+    }
+
+    /**
+     * Helper method to hide the actual value of the no children address.
+     */
+    private static boolean hasChildrenAddress(int address) {
+        return NO_CHILDREN_ADDRESS != address;
+    }
+
+    /**
+     * Compute the size, in bytes, that an address will occupy.
+     *
+     * This can be used either for children addresses (which are always positive) or for
+     * attribute, which may be positive or negative but
+     * store their sign bit separately.
+     *
+     * @param address the address
+     * @return the byte size.
+     */
+    private static int getByteSize(int address) {
+        assert(address < 0x1000000);
+        if (!hasChildrenAddress(address)) {
+            return 0;
+        } else if (Math.abs(address) < 0x100) {
+            return 1;
+        } else if (Math.abs(address) < 0x10000) {
+            return 2;
+        } else {
+            return 3;
+        }
+    }
+    // End utility methods.
+
+    // This method is responsible for finding a nice ordering of the nodes that favors run-time
+    // cache performance and dictionary size.
+    /* package for tests */ static ArrayList<Node> flattenTree(Node root) {
+        final int treeSize = FusionDictionary.countCharGroups(root);
+        MakedictLog.i("Counted nodes : " + treeSize);
+        final ArrayList<Node> flatTree = new ArrayList<Node>(treeSize);
+        return flattenTreeInner(flatTree, root);
+    }
+
+    private static ArrayList<Node> flattenTreeInner(ArrayList<Node> list, Node node) {
+        // Removing the node is necessary if the tails are merged, because we would then
+        // add the same node several times when we only want it once. A number of places in
+        // the code also depends on any node being only once in the list.
+        // Merging tails can only be done if there are no attributes. Searching for attributes
+        // in LatinIME code depends on a total breadth-first ordering, which merging tails
+        // breaks. If there are no attributes, it should be fine (and reduce the file size)
+        // to merge tails, and the following step would be necessary.
+        // If eventually the code runs on Android, searching through the whole array each time
+        // may be a performance concern.
+        list.remove(node);
+        list.add(node);
+        final ArrayList<CharGroup> branches = node.mData;
+        final int nodeSize = branches.size();
+        for (CharGroup group : branches) {
+            if (null != group.mChildren) flattenTreeInner(list, group.mChildren);
+        }
+        return list;
+    }
+
+    /**
+     * Finds the absolute address of a word in the dictionary.
+     *
+     * @param dict the dictionary in which to search.
+     * @param word the word we are searching for.
+     * @return the word address. If it is not found, an exception is thrown.
+     */
+    private static int findAddressOfWord(final FusionDictionary dict, final String word) {
+        return FusionDictionary.findWordInTree(dict.mRoot, word).mCachedAddress;
+    }
+
+    /**
+     * Computes the actual node size, based on the cached addresses of the children nodes.
+     *
+     * Each node stores its tentative address. During dictionary address computing, these
+     * are not final, but they can be used to compute the node size (the node size depends
+     * on the address of the children because the number of bytes necessary to store an
+     * address depends on its numeric value.
+     *
+     * @param node the node to compute the size of.
+     * @param dict the dictionary in which the word/attributes are to be found.
+     */
+    private static void computeActualNodeSize(Node node, FusionDictionary dict) {
+        int size = GROUP_COUNT_SIZE;
+        for (CharGroup group : node.mData) {
+            int groupSize = GROUP_FLAGS_SIZE + getGroupCharactersSize(group);
+            if (group.isTerminal()) groupSize += GROUP_FREQUENCY_SIZE;
+            if (null != group.mChildren) {
+                final int offsetBasePoint= groupSize + node.mCachedAddress + size;
+                final int offset = group.mChildren.mCachedAddress - offsetBasePoint;
+                groupSize += getByteSize(offset);
+            }
+            if (null != group.mBigrams) {
+                for (WeightedString bigram : group.mBigrams) {
+                    final int offsetBasePoint = groupSize + node.mCachedAddress + size
+                            + GROUP_FLAGS_SIZE;
+                    final int addressOfBigram = findAddressOfWord(dict, bigram.mWord);
+                    final int offset = addressOfBigram - offsetBasePoint;
+                    groupSize += getByteSize(offset) + GROUP_FLAGS_SIZE;
+                }
+            }
+            group.mCachedSize = groupSize;
+            size += groupSize;
+        }
+        node.mCachedSize = size;
+    }
+
+    /**
+     * Computes the byte size of a list of nodes and updates each node cached position.
+     *
+     * @param flatNodes the array of nodes.
+     * @return the byte size of the entire stack.
+     */
+    private static int stackNodes(ArrayList<Node> flatNodes) {
+        int nodeOffset = 0;
+        for (Node n : flatNodes) {
+            n.mCachedAddress = nodeOffset;
+            int groupOffset = 0;
+            for (CharGroup g : n.mData) {
+                g.mCachedAddress = GROUP_COUNT_SIZE + nodeOffset + groupOffset;
+                groupOffset += g.mCachedSize;
+            }
+            if (groupOffset + GROUP_COUNT_SIZE != n.mCachedSize) {
+                throw new RuntimeException("Bug : Stored and computed node size differ");
+            }
+            nodeOffset += n.mCachedSize;
+        }
+        return nodeOffset;
+    }
+
+    /**
+     * Compute the addresses and sizes of an ordered node array.
+     *
+     * This method takes a node array and will update its cached address and size values
+     * so that they can be written into a file. It determines the smallest size each of the
+     * nodes can be given the addresses of its children and attributes, and store that into
+     * each node.
+     * The order of the node is given by the order of the array. This method makes no effort
+     * to find a good order; it only mechanically computes the size this order results in.
+     *
+     * @param dict the dictionary
+     * @param flatNodes the ordered array of nodes
+     * @return the same array it was passed. The nodes have been updated for address and size.
+     */
+    private static ArrayList<Node> computeAddresses(FusionDictionary dict,
+            ArrayList<Node> flatNodes) {
+        // First get the worst sizes and offsets
+        for (Node n : flatNodes) setNodeMaximumSize(n);
+        final int offset = stackNodes(flatNodes);
+
+        MakedictLog.i("Compressing the array addresses. Original size : " + offset);
+        MakedictLog.i("(Recursively seen size : " + offset + ")");
+
+        int passes = 0;
+        boolean changesDone = false;
+        do {
+            changesDone = false;
+            for (Node n : flatNodes) {
+                final int oldNodeSize = n.mCachedSize;
+                computeActualNodeSize(n, dict);
+                final int newNodeSize = n.mCachedSize;
+                if (oldNodeSize < newNodeSize) throw new RuntimeException("Increased size ?!");
+                if (oldNodeSize != newNodeSize) changesDone = true;
+            }
+            stackNodes(flatNodes);
+            ++passes;
+        } while (changesDone);
+
+        final Node lastNode = flatNodes.get(flatNodes.size() - 1);
+        MakedictLog.i("Compression complete in " + passes + " passes.");
+        MakedictLog.i("After address compression : "
+                + (lastNode.mCachedAddress + lastNode.mCachedSize));
+
+        return flatNodes;
+    }
+
+    /**
+     * Sanity-checking method.
+     *
+     * This method checks an array of node for juxtaposition, that is, it will do
+     * nothing if each node's cached address is actually the previous node's address
+     * plus the previous node's size.
+     * If this is not the case, it will throw an exception.
+     *
+     * @param array the array node to check
+     */
+    private static void checkFlatNodeArray(ArrayList<Node> array) {
+        int offset = 0;
+        int index = 0;
+        for (Node n : array) {
+            if (n.mCachedAddress != offset) {
+                throw new RuntimeException("Wrong address for node " + index
+                        + " : expected " + offset + ", got " + n.mCachedAddress);
+            }
+            ++index;
+            offset += n.mCachedSize;
+        }
+    }
+
+    /**
+     * Helper method to write a variable-size address to a file.
+     *
+     * @param buffer the buffer to write to.
+     * @param index the index in the buffer to write the address to.
+     * @param address the address to write.
+     * @return the size in bytes the address actually took.
+     */
+    private static int writeVariableAddress(byte[] buffer, int index, int address) {
+        switch (getByteSize(address)) {
+        case 1:
+            buffer[index++] = (byte)address;
+            return 1;
+        case 2:
+            buffer[index++] = (byte)(0xFF & (address >> 8));
+            buffer[index++] = (byte)(0xFF & address);
+            return 2;
+        case 3:
+            buffer[index++] = (byte)(0xFF & (address >> 16));
+            buffer[index++] = (byte)(0xFF & (address >> 8));
+            buffer[index++] = (byte)(0xFF & address);
+            return 3;
+        case 0:
+            return 0;
+        default:
+            throw new RuntimeException("Address " + address + " has a strange size");
+        }
+    }
+
+    private static byte makeCharGroupFlags(final CharGroup group, final int groupAddress,
+            final int childrenOffset) {
+        byte flags = 0;
+        if (group.mChars.length > 1) flags |= FLAG_HAS_MULTIPLE_CHARS;
+        if (group.mFrequency >= 0) {
+            flags |= FLAG_IS_TERMINAL;
+        }
+        if (null != group.mChildren) {
+            switch (getByteSize(childrenOffset)) {
+             case 1:
+                 flags |= FLAG_GROUP_ADDRESS_TYPE_ONEBYTE;
+                 break;
+             case 2:
+                 flags |= FLAG_GROUP_ADDRESS_TYPE_TWOBYTES;
+                 break;
+             case 3:
+                 flags |= FLAG_GROUP_ADDRESS_TYPE_THREEBYTES;
+                 break;
+             default:
+                 throw new RuntimeException("Node with a strange address");
+             }
+        }
+        if (null != group.mBigrams) flags |= FLAG_HAS_BIGRAMS;
+        return flags;
+    }
+
+    /**
+     * Makes the flag value for an attribute.
+     *
+     * @param more whether there are more attributes after this one.
+     * @param offset the offset of the attribute.
+     * @param frequency the frequency of the attribute, 0..15
+     * @return the flags
+     */
+    private static final int makeAttributeFlags(final boolean more, final int offset,
+            final int frequency) {
+        int bigramFlags = (more ? FLAG_ATTRIBUTE_HAS_NEXT : 0)
+                + (offset < 0 ? FLAG_ATTRIBUTE_OFFSET_NEGATIVE : 0);
+        switch (getByteSize(offset)) {
+        case 1:
+            bigramFlags |= FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE;
+            break;
+        case 2:
+            bigramFlags |= FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES;
+            break;
+        case 3:
+            bigramFlags |= FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES;
+            break;
+        default:
+            throw new RuntimeException("Strange offset size");
+        }
+        bigramFlags += frequency & FLAG_ATTRIBUTE_FREQUENCY;
+        return bigramFlags;
+    }
+
+    /**
+     * Write a node to memory. The node is expected to have its final position cached.
+     *
+     * This can be an empty map, but the more is inside the faster the lookups will be. It can
+     * be carried on as long as nodes do not move.
+     *
+     * @param dict the dictionary the node is a part of (for relative offsets).
+     * @param buffer the memory buffer to write to.
+     * @param node the node to write.
+     * @return the address of the END of the node.
+     */
+    private static int writePlacedNode(FusionDictionary dict, byte[] buffer, Node node) {
+        int index = node.mCachedAddress;
+
+        final int size = node.mData.size();
+        if (size > MAX_CHARGROUPS_IN_A_NODE)
+            throw new RuntimeException("A node has a group count over 127 (" + size + ").");
+
+        buffer[index++] = (byte)size;
+        int groupAddress = index;
+        for (int i = 0; i < size; ++i) {
+            CharGroup group = node.mData.get(i);
+            if (index != group.mCachedAddress) throw new RuntimeException("Bug: write index is not "
+                    + "the same as the cached address of the group");
+            groupAddress += GROUP_FLAGS_SIZE + getGroupCharactersSize(group);
+            // Sanity checks.
+            if (group.mFrequency > MAX_TERMINAL_FREQUENCY) {
+                throw new RuntimeException("A node has a frequency > " + MAX_TERMINAL_FREQUENCY
+                        + " : " + group.mFrequency);
+            }
+            if (group.mFrequency >= 0) groupAddress += GROUP_FREQUENCY_SIZE;
+            final int childrenOffset = null == group.mChildren
+                    ? NO_CHILDREN_ADDRESS : group.mChildren.mCachedAddress - groupAddress;
+            byte flags = makeCharGroupFlags(group, groupAddress, childrenOffset);
+            buffer[index++] = flags;
+            index = CharEncoding.writeCharArray(group.mChars, buffer, index);
+            if (group.hasSeveralChars()) {
+                buffer[index++] = GROUP_CHARACTERS_TERMINATOR;
+            }
+            if (group.mFrequency >= 0) {
+                buffer[index++] = (byte) group.mFrequency;
+            }
+            final int shift = writeVariableAddress(buffer, index, childrenOffset);
+            index += shift;
+            groupAddress += shift;
+
+            // Write bigrams
+            if (null != group.mBigrams) {
+                int remainingBigrams = group.mBigrams.size();
+                for (WeightedString bigram : group.mBigrams) {
+                    boolean more = remainingBigrams > 1;
+                    final int addressOfBigram = findAddressOfWord(dict, bigram.mWord);
+                    ++groupAddress;
+                    final int offset = addressOfBigram - groupAddress;
+                    int bigramFlags = makeAttributeFlags(more, offset, bigram.mFrequency);
+                    buffer[index++] = (byte)bigramFlags;
+                    final int bigramShift = writeVariableAddress(buffer, index, Math.abs(offset));
+                    index += bigramShift;
+                    groupAddress += bigramShift;
+                    --remainingBigrams;
+                }
+            }
+
+        }
+        if (index != node.mCachedAddress + node.mCachedSize) throw new RuntimeException(
+                "Not the same size : written "
+                + (index - node.mCachedAddress) + " bytes out of a node that should have "
+                + node.mCachedSize + " bytes");
+        return index;
+    }
+
+    /**
+     * Dumps a collection of useful statistics about a node array.
+     *
+     * This prints purely informative stuff, like the total estimated file size, the
+     * number of nodes, of character groups, the repartition of each address size, etc
+     *
+     * @param nodes the node array.
+     */
+    private static void showStatistics(ArrayList<Node> nodes) {
+        int firstTerminalAddress = Integer.MAX_VALUE;
+        int lastTerminalAddress = Integer.MIN_VALUE;
+        int size = 0;
+        int charGroups = 0;
+        int maxGroups = 0;
+        int maxRuns = 0;
+        for (Node n : nodes) {
+            if (maxGroups < n.mData.size()) maxGroups = n.mData.size();
+            for (CharGroup cg : n.mData) {
+                ++charGroups;
+                if (cg.mChars.length > maxRuns) maxRuns = cg.mChars.length;
+                if (cg.mFrequency >= 0) {
+                    if (n.mCachedAddress < firstTerminalAddress)
+                        firstTerminalAddress = n.mCachedAddress;
+                    if (n.mCachedAddress > lastTerminalAddress)
+                        lastTerminalAddress = n.mCachedAddress;
+                }
+            }
+            if (n.mCachedAddress + n.mCachedSize > size) size = n.mCachedAddress + n.mCachedSize;
+        }
+        final int[] groupCounts = new int[maxGroups + 1];
+        final int[] runCounts = new int[maxRuns + 1];
+        for (Node n : nodes) {
+            ++groupCounts[n.mData.size()];
+            for (CharGroup cg : n.mData) {
+                ++runCounts[cg.mChars.length];
+            }
+        }
+
+        MakedictLog.i("Statistics:\n"
+                + "  total file size " + size + "\n"
+                + "  " + nodes.size() + " nodes\n"
+                + "  " + charGroups + " groups (" + ((float)charGroups / nodes.size())
+                        + " groups per node)\n"
+                + "  first terminal at " + firstTerminalAddress + "\n"
+                + "  last terminal at " + lastTerminalAddress + "\n"
+                + "  Group stats : max = " + maxGroups);
+        for (int i = 0; i < groupCounts.length; ++i) {
+            MakedictLog.i("    " + i + " : " + groupCounts[i]);
+        }
+        MakedictLog.i("  Character run stats : max = " + maxRuns);
+        for (int i = 0; i < runCounts.length; ++i) {
+            MakedictLog.i("    " + i + " : " + runCounts[i]);
+        }
+    }
+
+    /**
+     * Dumps a FusionDictionary to a file.
+     *
+     * This is the public entry point to write a dictionary to a file.
+     *
+     * @param destination the stream to write the binary data to.
+     * @param dict the dictionary to write.
+     */
+    public static void writeDictionaryBinary(OutputStream destination, FusionDictionary dict)
+            throws IOException {
+
+        // Addresses are limited to 3 bytes, so we'll just make a 16MB buffer. Since addresses
+        // can be relative to each node, the structure itself is not limited to 16MB at all, but
+        // I doubt this will ever be shot. If it is, deciding the order of the nodes becomes
+        // a quite complicated problem, because though the dictionary itself does not have a
+        // size limit, each node must still be within 16MB of all its children and parents.
+        // As long as this is ensured, the dictionary file may grow to any size.
+        // Anyway, to make a dictionary bigger than 16MB just increase the size of this buffer.
+        final byte[] buffer = new byte[1 << 24];
+        int index = 0;
+
+        // Magic number in big-endian order.
+        buffer[index++] = (byte) (0xFF & (MAGIC_NUMBER >> 8));
+        buffer[index++] = (byte) (0xFF & MAGIC_NUMBER);
+        // Dictionary version.
+        buffer[index++] = (byte) (0xFF & VERSION);
+        // Options flags
+        buffer[index++] = (byte) (0xFF & (OPTIONS >> 8));
+        buffer[index++] = (byte) (0xFF & OPTIONS);
+
+        // Should we include the locale and title of the dictionary ?
+
+        destination.write(buffer, 0, index);
+        index = 0;
+
+        // Leave the choice of the optimal node order to the flattenTree function.
+        MakedictLog.i("Flattening the tree...");
+        ArrayList<Node> flatNodes = flattenTree(dict.mRoot);
+
+        MakedictLog.i("Computing addresses...");
+        computeAddresses(dict, flatNodes);
+        MakedictLog.i("Checking array...");
+        checkFlatNodeArray(flatNodes);
+
+        MakedictLog.i("Writing file...");
+        int dataEndOffset = 0;
+        for (Node n : flatNodes) {
+            dataEndOffset = writePlacedNode(dict, buffer, n);
+        }
+
+        showStatistics(flatNodes);
+
+        destination.write(buffer, 0, dataEndOffset);
+
+        destination.close();
+        MakedictLog.i("Done");
+    }
+
+
+    // Input methods: Read a binary dictionary to memory.
+    // readDictionaryBinary is the public entry point for them.
+
+    static final int[] characterBuffer = new int[MAX_WORD_LENGTH];
+    private static CharGroupInfo readCharGroup(RandomAccessFile source,
+            final int originalGroupAddress) throws IOException {
+        int addressPointer = originalGroupAddress;
+        final int flags = source.readUnsignedByte();
+        ++addressPointer;
+        final int characters[];
+        if (0 != (flags & FLAG_HAS_MULTIPLE_CHARS)) {
+            int index = 0;
+            int character = CharEncoding.readChar(source);
+            addressPointer += CharEncoding.getCharSize(character);
+            while (-1 != character) {
+                characterBuffer[index++] = character;
+                character = CharEncoding.readChar(source);
+                addressPointer += CharEncoding.getCharSize(character);
+            }
+            characters = Arrays.copyOfRange(characterBuffer, 0, index);
+        } else {
+            final int character = CharEncoding.readChar(source);
+            addressPointer += CharEncoding.getCharSize(character);
+            characters = new int[] { character };
+        }
+        final int frequency;
+        if (0 != (FLAG_IS_TERMINAL & flags)) {
+            ++addressPointer;
+            frequency = source.readUnsignedByte();
+        } else {
+            frequency = CharGroup.NOT_A_TERMINAL;
+        }
+        int childrenAddress = addressPointer;
+        switch (flags & MASK_GROUP_ADDRESS_TYPE) {
+        case FLAG_GROUP_ADDRESS_TYPE_ONEBYTE:
+            childrenAddress += source.readUnsignedByte();
+            addressPointer += 1;
+            break;
+        case FLAG_GROUP_ADDRESS_TYPE_TWOBYTES:
+            childrenAddress += source.readUnsignedShort();
+            addressPointer += 2;
+            break;
+        case FLAG_GROUP_ADDRESS_TYPE_THREEBYTES:
+            childrenAddress += (source.readUnsignedByte() << 16) + source.readUnsignedShort();
+            addressPointer += 3;
+            break;
+        case FLAG_GROUP_ADDRESS_TYPE_NOADDRESS:
+        default:
+            childrenAddress = NO_CHILDREN_ADDRESS;
+            break;
+        }
+        ArrayList<PendingAttribute> bigrams = null;
+        if (0 != (flags & FLAG_HAS_BIGRAMS)) {
+            bigrams = new ArrayList<PendingAttribute>();
+            boolean more = true;
+            while (more) {
+                int bigramFlags = source.readUnsignedByte();
+                ++addressPointer;
+                more = (0 != (bigramFlags & FLAG_ATTRIBUTE_HAS_NEXT));
+                final int sign = 0 == (bigramFlags & FLAG_ATTRIBUTE_OFFSET_NEGATIVE) ? 1 : -1;
+                int bigramAddress = addressPointer;
+                switch (bigramFlags & MASK_ATTRIBUTE_ADDRESS_TYPE) {
+                case FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE:
+                    bigramAddress += sign * source.readUnsignedByte();
+                    addressPointer += 1;
+                    break;
+                case FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES:
+                    bigramAddress += sign * source.readUnsignedShort();
+                    addressPointer += 2;
+                    break;
+                case FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES:
+                    final int offset = ((source.readUnsignedByte() << 16)
+                            + source.readUnsignedShort());
+                    bigramAddress += sign * offset;
+                    addressPointer += 3;
+                    break;
+                default:
+                    throw new RuntimeException("Has attribute with no address");
+                }
+                bigrams.add(new PendingAttribute(bigramFlags & FLAG_ATTRIBUTE_FREQUENCY,
+                        bigramAddress));
+            }
+        }
+        return new CharGroupInfo(originalGroupAddress, addressPointer, flags, characters, frequency,
+                childrenAddress, bigrams);
+    }
+
+    /**
+     * Finds, as a string, the word at the address passed as an argument.
+     *
+     * @param source the file to read from.
+     * @param headerSize the size of the header.
+     * @param address the address to seek.
+     * @return the word, as a string.
+     * @throws IOException if the file can't be read.
+     */
+    private static String getWordAtAddress(RandomAccessFile source, long headerSize,
+            int address) throws IOException {
+        final long originalPointer = source.getFilePointer();
+        source.seek(headerSize);
+        final int count = source.readUnsignedByte();
+        int groupOffset = 1; // 1 for the group count
+        final StringBuilder builder = new StringBuilder();
+        String result = null;
+
+        CharGroupInfo last = null;
+        for (int i = count - 1; i >= 0; --i) {
+            CharGroupInfo info = readCharGroup(source, groupOffset);
+            groupOffset = info.mEndAddress;
+            if (info.mOriginalAddress == address) {
+                builder.append(new String(info.mCharacters, 0, info.mCharacters.length));
+                result = builder.toString();
+                break; // and return
+            }
+            if (hasChildrenAddress(info.mChildrenAddress)) {
+                if (info.mChildrenAddress > address) {
+                    if (null == last) continue;
+                    builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
+                    source.seek(last.mChildrenAddress + headerSize);
+                    groupOffset = last.mChildrenAddress + 1;
+                    i = source.readUnsignedByte();
+                    last = null;
+                    continue;
+                }
+                last = info;
+            }
+            if (0 == i && hasChildrenAddress(last.mChildrenAddress)) {
+                builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
+                source.seek(last.mChildrenAddress + headerSize);
+                groupOffset = last.mChildrenAddress + 1;
+                i = source.readUnsignedByte();
+                last = null;
+                continue;
+            }
+        }
+        source.seek(originalPointer);
+        return result;
+    }
+
+    /**
+     * Reads a single node from a binary file.
+     *
+     * This methods reads the file at the current position of its file pointer. A node is
+     * fully expected to start at the current position.
+     * This will recursively read other nodes into the structure, populating the reverse
+     * maps on the fly and using them to keep track of already read nodes.
+     *
+     * @param source the data file, correctly positioned at the start of a node.
+     * @param headerSize the size, in bytes, of the file header.
+     * @param reverseNodeMap a mapping from addresses to already read nodes.
+     * @param reverseGroupMap a mapping from addresses to already read character groups.
+     * @return the read node with all his children already read.
+     */
+    private static Node readNode(RandomAccessFile source, long headerSize,
+            Map<Integer, Node> reverseNodeMap, Map<Integer, CharGroup> reverseGroupMap)
+            throws IOException {
+        final int nodeOrigin = (int)(source.getFilePointer() - headerSize);
+        final int count = source.readUnsignedByte();
+        final ArrayList<CharGroup> nodeContents = new ArrayList<CharGroup>();
+        int groupOffset = nodeOrigin + 1; // 1 byte for the group count
+        for (int i = count; i > 0; --i) {
+            CharGroupInfo info = readCharGroup(source, groupOffset);
+            ArrayList<WeightedString> bigrams = null;
+            if (null != info.mBigrams) {
+                bigrams = new ArrayList<WeightedString>();
+                for (PendingAttribute bigram : info.mBigrams) {
+                    final String word = getWordAtAddress(source, headerSize, bigram.mAddress);
+                    bigrams.add(new WeightedString(word, bigram.mFrequency));
+                }
+            }
+            if (hasChildrenAddress(info.mChildrenAddress)) {
+                Node children = reverseNodeMap.get(info.mChildrenAddress);
+                if (null == children) {
+                    final long currentPosition = source.getFilePointer();
+                    source.seek(info.mChildrenAddress + headerSize);
+                    children = readNode(source, headerSize, reverseNodeMap, reverseGroupMap);
+                    source.seek(currentPosition);
+                }
+                nodeContents.add(
+                        new CharGroup(info.mCharacters, bigrams, info.mFrequency,
+                        children));
+            } else {
+                nodeContents.add(
+                        new CharGroup(info.mCharacters, bigrams, info.mFrequency));
+            }
+            groupOffset = info.mEndAddress;
+        }
+        final Node node = new Node(nodeContents);
+        node.mCachedAddress = nodeOrigin;
+        reverseNodeMap.put(node.mCachedAddress, node);
+        return node;
+    }
+
+    /**
+     * Reads a random access file and returns the memory representation of the dictionary.
+     *
+     * This high-level method takes a binary file and reads its contents, populating a
+     * FusionDictionary structure. The optional dict argument is an existing dictionary to
+     * which words from the file should be added. If it is null, a new dictionary is created.
+     *
+     * @param source the file to read.
+     * @param dict an optional dictionary to add words to, or null.
+     * @return the created (or merged) dictionary.
+     */
+    public static FusionDictionary readDictionaryBinary(RandomAccessFile source,
+            FusionDictionary dict) throws IOException, UnsupportedFormatException {
+        // Check magic number
+        final int magic = source.readUnsignedShort();
+        if (MAGIC_NUMBER != magic) {
+            throw new UnsupportedFormatException("The magic number in this file does not match "
+                    + "the expected value");
+        }
+
+        // Check file version
+        final int version = source.readUnsignedByte();
+        if (version > MAXIMUM_SUPPORTED_VERSION) {
+            throw new UnsupportedFormatException("This file has version " + version
+                    + ", but this implementation does not support versions above "
+                    + MAXIMUM_SUPPORTED_VERSION);
+        }
+
+        // Read options
+        source.readUnsignedShort();
+
+        long headerSize = source.getFilePointer();
+        Map<Integer, Node> reverseNodeMapping = new TreeMap<Integer, Node>();
+        Map<Integer, CharGroup> reverseGroupMapping = new TreeMap<Integer, CharGroup>();
+        final Node root = readNode(source, headerSize, reverseNodeMapping, reverseGroupMapping);
+
+        FusionDictionary newDict = new FusionDictionary(root,
+                new FusionDictionary.DictionaryOptions());
+        if (null != dict) {
+            for (Word w : dict) {
+                newDict.add(w.mWord, w.mFrequency, w.mBigrams);
+            }
+        }
+
+        return newDict;
+    }
+
+    /**
+     * Basic test to find out whether the file is a binary dictionary or not.
+     *
+     * Concretely this only tests the magic number.
+     *
+     * @param filename The name of the file to test.
+     * @return true if it's a binary dictionary, false otherwise
+     */
+    public static boolean isBinaryDictionary(String filename) {
+        try {
+            RandomAccessFile f = new RandomAccessFile(filename, "r");
+            return MAGIC_NUMBER == f.readUnsignedShort();
+        } catch (FileNotFoundException e) {
+            return false;
+        } catch (IOException e) {
+            return false;
+        }
+    }
+}
diff --git a/tools/makedict2/src/com/android/inputmethod/latin/CharGroupInfo.java b/tools/makedict2/src/com/android/inputmethod/latin/CharGroupInfo.java
new file mode 100644
index 0000000000000000000000000000000000000000..6badfd13a014b7573135b639bb9c8f43138c46cc
--- /dev/null
+++ b/tools/makedict2/src/com/android/inputmethod/latin/CharGroupInfo.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package com.android.inputmethod.latin;
+
+import java.util.ArrayList;
+
+/**
+ * Raw char group info straight out of a file. This will contain numbers for addresses.
+ */
+public class CharGroupInfo {
+
+    public final int mOriginalAddress;
+    public final int mEndAddress;
+    public final int mFlags;
+    public final int[] mCharacters;
+    public final int mFrequency;
+    public final int mChildrenAddress;
+    public final ArrayList<PendingAttribute> mBigrams;
+
+    public CharGroupInfo(final int originalAddress, final int endAddress, final int flags,
+            final int[] characters, final int frequency, final int childrenAddress,
+            final ArrayList<PendingAttribute> bigrams) {
+        mOriginalAddress = originalAddress;
+        mEndAddress = endAddress;
+        mFlags = flags;
+        mCharacters = characters;
+        mFrequency = frequency;
+        mChildrenAddress = childrenAddress;
+        mBigrams = bigrams;
+    }
+}
diff --git a/tools/makedict2/src/com/android/inputmethod/latin/DictionaryMaker.java b/tools/makedict2/src/com/android/inputmethod/latin/DictionaryMaker.java
new file mode 100644
index 0000000000000000000000000000000000000000..61944545ad2abe9c84824b370056f9c2351635ae
--- /dev/null
+++ b/tools/makedict2/src/com/android/inputmethod/latin/DictionaryMaker.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package com.android.inputmethod.latin;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.Arrays;
+import java.util.LinkedList;
+
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.xml.sax.SAXException;
+
+/**
+ * Main class/method for DictionaryMaker.
+ */
+public class DictionaryMaker {
+
+    static class Arguments {
+        private final static String OPTION_VERSION_2 = "-2";
+        private final static String OPTION_INPUT_SOURCE = "-s";
+        private final static String OPTION_INPUT_BIGRAM_XML = "-b";
+        private final static String OPTION_OUTPUT_BINARY = "-d";
+        private final static String OPTION_OUTPUT_XML = "-x";
+        private final static String OPTION_HELP = "-h";
+        public final String mInputBinary;
+        public final String mInputUnigramXml;
+        public final String mInputBigramXml;
+        public final String mOutputBinary;
+        public final String mOutputXml;
+
+        private void checkIntegrity() {
+            checkHasExactlyOneInput();
+            checkHasAtLeastOneOutput();
+        }
+
+        private void checkHasExactlyOneInput() {
+            if (null == mInputUnigramXml && null == mInputBinary) {
+                throw new RuntimeException("No input file specified");
+            } else if (null != mInputUnigramXml && null != mInputBinary) {
+                throw new RuntimeException("Both input XML and binary specified");
+            } else if (null != mInputBinary && null != mInputBigramXml) {
+                throw new RuntimeException("Cannot specify a binary input and a separate bigram "
+                        + "file");
+            }
+        }
+
+        private void checkHasAtLeastOneOutput() {
+            if (null == mOutputBinary && null == mOutputXml) {
+                throw new RuntimeException("No output specified");
+            }
+        }
+
+        private void displayHelp() {
+            MakedictLog.i("Usage: makedict2 "
+                    + "[-s <unigrams.xml> [-b <bigrams.xml>] | -s <binary input>] "
+                    + " [-d <binary output>] [-x <xml output>] [-2]\n"
+                    + "\n"
+                    + "  Converts a source dictionary file to one or several outputs.\n"
+                    + "  Source can be an XML file, with an optional XML bigrams file, or a\n"
+                    + "  binary dictionary file.\n"
+                    + "  Both binary and XML outputs are supported. Both can be output at\n"
+                    + "  the same time but outputting several files of the same type is not\n"
+                    + "  supported.");
+        }
+
+        public Arguments(String[] argsArray) {
+            final LinkedList<String> args = new LinkedList<String>(Arrays.asList(argsArray));
+            if (args.isEmpty()) {
+                displayHelp();
+            }
+            String inputBinary = null;
+            String inputUnigramXml = null;
+            String inputBigramXml = null;
+            String outputBinary = null;
+            String outputXml = null;
+
+            while (!args.isEmpty()) {
+                final String arg = args.get(0);
+                args.remove(0);
+                if (arg.charAt(0) == '-') {
+                    if (OPTION_VERSION_2.equals(arg)) {
+                        // Do nothing, this is the default
+                    } else if (OPTION_HELP.equals(arg)) {
+                        displayHelp();
+                    } else {
+                        // All these options need an argument
+                        if (args.isEmpty()) {
+                            throw new RuntimeException("Option " + arg + " requires an argument");
+                        }
+                        String filename = args.get(0);
+                        args.remove(0);
+                        if (OPTION_INPUT_SOURCE.equals(arg)) {
+                            if (BinaryDictInputOutput.isBinaryDictionary(filename)) {
+                                inputBinary = filename;
+                            } else {
+                                inputUnigramXml = filename;
+                            }
+                        } else if (OPTION_INPUT_BIGRAM_XML.equals(arg)) {
+                            inputBigramXml = filename;
+                        } else if (OPTION_OUTPUT_BINARY.equals(arg)) {
+                            outputBinary = filename;
+                        } else if (OPTION_OUTPUT_XML.equals(arg)) {
+                            outputXml = filename;
+                        }
+                    }
+                } else {
+                    if (null == inputBinary && null == inputUnigramXml) {
+                        if (BinaryDictInputOutput.isBinaryDictionary(arg)) {
+                            inputBinary = arg;
+                        } else {
+                            inputUnigramXml = arg;
+                        }
+                    } else if (null == outputBinary) {
+                        outputBinary = arg;
+                    } else {
+                        throw new RuntimeException("Several output binary files specified");
+                    }
+                }
+            }
+
+            mInputBinary = inputBinary;
+            mInputUnigramXml = inputUnigramXml;
+            mInputBigramXml = inputBigramXml;
+            mOutputBinary = outputBinary;
+            mOutputXml = outputXml;
+            checkIntegrity();
+        }
+    }
+
+    public static void main(String[] args)
+            throws FileNotFoundException, ParserConfigurationException, SAXException, IOException,
+            UnsupportedFormatException {
+        final Arguments parsedArgs = new Arguments(args);
+        FusionDictionary dictionary = readInputFromParsedArgs(parsedArgs);
+        writeOutputToParsedArgs(parsedArgs, dictionary);
+    }
+
+    /**
+     * Invoke the right input method according to args.
+     *
+     * @param args the parsed command line arguments.
+     * @return the read dictionary.
+     */
+    private static FusionDictionary readInputFromParsedArgs(final Arguments args)
+            throws IOException, UnsupportedFormatException, ParserConfigurationException,
+            SAXException, FileNotFoundException {
+        if (null != args.mInputBinary) {
+            return readBinaryFile(args.mInputBinary);
+        } else if (null != args.mInputUnigramXml) {
+            return readXmlFile(args.mInputUnigramXml, args.mInputBigramXml);
+        } else {
+            throw new RuntimeException("No input file specified");
+        }
+    }
+
+    /**
+     * Read a dictionary from the name of a binary file.
+     *
+     * @param binaryFilename the name of the file in the binary dictionary format.
+     * @return the read dictionary.
+     * @throws FileNotFoundException if the file can't be found
+     * @throws IOException if the input file can't be read
+     * @throws UnsupportedFormatException if the binary file is not in the expected format
+     */
+    private static FusionDictionary readBinaryFile(final String binaryFilename)
+            throws FileNotFoundException, IOException, UnsupportedFormatException {
+        final RandomAccessFile inputFile = new RandomAccessFile(binaryFilename, "r");
+        return BinaryDictInputOutput.readDictionaryBinary(inputFile, null);
+    }
+
+    /**
+     * Read a dictionary from a unigram XML file, and optionally a bigram XML file.
+     *
+     * @param unigramXmlFilename the name of the unigram XML file. May not be null.
+     * @param bigramXmlFilename the name of the bigram XML file. Pass null if there are no bigrams.
+     * @return the read dictionary.
+     * @throws FileNotFoundException if one of the files can't be found
+     * @throws SAXException if one or more of the XML files is not well-formed
+     * @throws IOException if one the input files can't be read
+     * @throws ParserConfigurationException if the system can't create a SAX parser
+     */
+    private static FusionDictionary readXmlFile(final String unigramXmlFilename,
+            final String bigramXmlFilename) throws FileNotFoundException, SAXException,
+            IOException, ParserConfigurationException {
+        final FileInputStream unigrams = new FileInputStream(new File(unigramXmlFilename));
+        final FileInputStream bigrams = null == bigramXmlFilename ? null :
+                new FileInputStream(new File(bigramXmlFilename));
+        return XmlDictInputOutput.readDictionaryXml(unigrams, bigrams);
+    }
+
+    /**
+     * Invoke the right output method according to args.
+     *
+     * This will write the passed dictionary to the file(s) passed in the command line arguments.
+     * @param args the parsed arguments.
+     * @param dict the file to output.
+     * @throws FileNotFoundException if one of the output files can't be created.
+     * @throws IOException if one of the output files can't be written to.
+     */
+    private static void writeOutputToParsedArgs(final Arguments args, final FusionDictionary dict)
+            throws FileNotFoundException, IOException {
+        if (null != args.mOutputBinary) {
+            writeBinaryDictionary(args.mOutputBinary, dict);
+        }
+        if (null != args.mOutputXml) {
+            writeXmlDictionary(args.mOutputXml, dict);
+        }
+    }
+
+    /**
+     * Write the dictionary in binary format to the specified filename.
+     *
+     * @param outputFilename the name of the file to write to.
+     * @param dict the dictionary to write.
+     * @throws FileNotFoundException if the output file can't be created.
+     * @throws IOException if the output file can't be written to.
+     */
+    private static void writeBinaryDictionary(final String outputFilename,
+            final FusionDictionary dict) throws FileNotFoundException, IOException {
+        final File outputFile = new File(outputFilename);
+        BinaryDictInputOutput.writeDictionaryBinary(new FileOutputStream(outputFilename), dict);
+    }
+
+    /**
+     * Write the dictionary in XML format to the specified filename.
+     *
+     * @param outputFilename the name of the file to write to.
+     * @param dict the dictionary to write.
+     * @throws FileNotFoundException if the output file can't be created.
+     * @throws IOException if the output file can't be written to.
+     */
+    private static void writeXmlDictionary(final String outputFilename,
+            final FusionDictionary dict) throws FileNotFoundException, IOException {
+        XmlDictInputOutput.writeDictionaryXml(new FileWriter(outputFilename), dict);
+    }
+}
diff --git a/tools/makedict2/src/com/android/inputmethod/latin/FusionDictionary.java b/tools/makedict2/src/com/android/inputmethod/latin/FusionDictionary.java
new file mode 100644
index 0000000000000000000000000000000000000000..031f35dfe06b8b65c1038dd92c5597b304b95682
--- /dev/null
+++ b/tools/makedict2/src/com/android/inputmethod/latin/FusionDictionary.java
@@ -0,0 +1,602 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package com.android.inputmethod.latin;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A dictionary that can fusion heads and tails of words for more compression.
+ */
+public class FusionDictionary implements Iterable<Word> {
+
+    /**
+     * A node of the dictionary, containing several CharGroups.
+     *
+     * A node is but an ordered array of CharGroups, which essentially contain all the
+     * real information.
+     * This class also contains fields to cache size and address, to help with binary
+     * generation.
+     */
+    public static class Node {
+        ArrayList<CharGroup> mData;
+        // To help with binary generation
+        int mCachedSize;
+        int mCachedAddress;
+        public Node() {
+            mData = new ArrayList<CharGroup>();
+            mCachedSize = Integer.MIN_VALUE;
+            mCachedAddress = Integer.MIN_VALUE;
+        }
+        public Node(ArrayList<CharGroup> data) {
+            mData = data;
+            mCachedSize = Integer.MIN_VALUE;
+            mCachedAddress = Integer.MIN_VALUE;
+        }
+    }
+
+    /**
+     * A string with a frequency.
+     *
+     * This represents an "attribute", that is either a bigram or a shortcut.
+     */
+    public static class WeightedString {
+        final String mWord;
+        final int mFrequency;
+        public WeightedString(String word, int frequency) {
+            mWord = word;
+            mFrequency = frequency;
+        }
+    }
+
+    /**
+     * A group of characters, with a frequency, shortcuts, bigrams, and children.
+     *
+     * This is the central class of the in-memory representation. A CharGroup is what can
+     * be seen as a traditional "trie node", except it can hold several characters at the
+     * same time. A CharGroup essentially represents one or several characters in the middle
+     * of the trie trie; as such, it can be a terminal, and it can have children.
+     * In this in-memory representation, whether the CharGroup is a terminal or not is represented
+     * in the frequency, where NOT_A_TERMINAL (= -1) means this is not a terminal and any other
+     * value is the frequency of this terminal. A terminal may have non-null shortcuts and/or
+     * bigrams, but a non-terminal may not. Moreover, children, if present, are null.
+     */
+    public static class CharGroup {
+        public static final int NOT_A_TERMINAL = -1;
+        final int mChars[];
+        final ArrayList<WeightedString> mBigrams;
+        final int mFrequency; // NOT_A_TERMINAL == mFrequency indicates this is not a terminal.
+        Node mChildren;
+        // The two following members to help with binary generation
+        int mCachedSize;
+        int mCachedAddress;
+
+        public CharGroup(final int[] chars,
+                final ArrayList<WeightedString> bigrams, final int frequency) {
+            mChars = chars;
+            mFrequency = frequency;
+            mBigrams = bigrams;
+            mChildren = null;
+        }
+
+        public CharGroup(final int[] chars,
+                final ArrayList<WeightedString> bigrams, final int frequency, final Node children) {
+            mChars = chars;
+            mFrequency = frequency;
+            mBigrams = bigrams;
+            mChildren = children;
+        }
+
+        public void addChild(CharGroup n) {
+            if (null == mChildren) {
+                mChildren = new Node();
+            }
+            mChildren.mData.add(n);
+        }
+
+        public boolean isTerminal() {
+            return NOT_A_TERMINAL != mFrequency;
+        }
+
+        public boolean hasSeveralChars() {
+            assert(mChars.length > 0);
+            return 1 < mChars.length;
+        }
+    }
+
+    /**
+     * Options global to the dictionary.
+     *
+     * There are no options at the moment, so this class is empty.
+     */
+    public static class DictionaryOptions {
+    }
+
+
+    public final DictionaryOptions mOptions;
+    public final Node mRoot;
+
+    public FusionDictionary() {
+        mOptions = new DictionaryOptions();
+        mRoot = new Node();
+    }
+
+    public FusionDictionary(final Node root, final DictionaryOptions options) {
+        mRoot = root;
+        mOptions = options;
+    }
+
+    /**
+     * Helper method to convert a String to an int array.
+     */
+    static private int[] getCodePoints(String word) {
+        final int wordLength = word.length();
+        int[] array = new int[word.codePointCount(0, wordLength)];
+        for (int i = 0; i < wordLength; ++i) {
+            array[i] = word.codePointAt(i);
+        }
+        return array;
+    }
+
+    /**
+     * Helper method to add a word as a string.
+     *
+     * This method adds a word to the dictionary with the given frequency. Optional
+     * lists of bigrams and shortcuts can be passed here. For each word inside,
+     * they will be added to the dictionary as necessary.
+     *
+     * @param word the word to add.
+     * @param frequency the frequency of the word, in the range [0..255].
+     * @param bigrams a list of bigrams, or null.
+     */
+    public void add(String word, int frequency, ArrayList<WeightedString> bigrams) {
+        if (null != bigrams) {
+            for (WeightedString bigram : bigrams) {
+                final CharGroup t = findWordInTree(mRoot, bigram.mWord);
+                if (null == t) {
+                    add(getCodePoints(bigram.mWord), 0, null);
+                }
+            }
+        }
+        add(getCodePoints(word), frequency, bigrams);
+    }
+
+    /**
+     * Sanity check for a node.
+     *
+     * This method checks that all CharGroups in a node are ordered as expected.
+     * If they are, nothing happens. If they aren't, an exception is thrown.
+     */
+    private void checkStack(Node node) {
+        ArrayList<CharGroup> stack = node.mData;
+        int lastValue = -1;
+        for (int i = 0; i < stack.size(); ++i) {
+            int currentValue = stack.get(i).mChars[0];
+            if (currentValue <= lastValue)
+                throw new RuntimeException("Invalid stack");
+            else
+                lastValue = currentValue;
+        }
+    }
+
+    /**
+     * Add a word to this dictionary.
+     *
+     * The bigrams, if any, have to be in the dictionary already. If they aren't,
+     * an exception is thrown.
+     *
+     * @param word the word, as an int array.
+     * @param frequency the frequency of the word, in the range [0..255].
+     * @param bigrams an optional list of bigrams for this word (null if none).
+     */
+    private void add(int[] word, int frequency, ArrayList<WeightedString> bigrams) {
+        assert(frequency >= 0 && frequency <= 255);
+        Node currentNode = mRoot;
+        int charIndex = 0;
+
+        CharGroup currentGroup = null;
+        int differentCharIndex = 0; // Set by the loop to the index of the char that differs
+        int nodeIndex = findIndexOfChar(mRoot, word[charIndex]);
+        while (CHARACTER_NOT_FOUND != nodeIndex) {
+            currentGroup = currentNode.mData.get(nodeIndex);
+            differentCharIndex = compareArrays(currentGroup.mChars, word, charIndex) ;
+            if (ARRAYS_ARE_EQUAL != differentCharIndex
+                    && differentCharIndex < currentGroup.mChars.length) break;
+            if (null == currentGroup.mChildren) break;
+            charIndex += currentGroup.mChars.length;
+            if (charIndex >= word.length) break;
+            currentNode = currentGroup.mChildren;
+            nodeIndex = findIndexOfChar(currentNode, word[charIndex]);
+        }
+
+        if (-1 == nodeIndex) {
+            // No node at this point to accept the word. Create one.
+            final int insertionIndex = findInsertionIndex(currentNode, word[charIndex]);
+            final CharGroup newGroup = new CharGroup(
+                    Arrays.copyOfRange(word, charIndex, word.length), bigrams, frequency);
+            currentNode.mData.add(insertionIndex, newGroup);
+            checkStack(currentNode);
+        } else {
+            // There is a word with a common prefix.
+            if (differentCharIndex == currentGroup.mChars.length) {
+                if (charIndex + differentCharIndex >= word.length) {
+                    // The new word is a prefix of an existing word, but the node on which it
+                    // should end already exists as is.
+                    if (currentGroup.mFrequency > 0) {
+                        throw new RuntimeException("Such a word already exists in the dictionary : "
+                                + new String(word, 0, word.length));
+                    } else {
+                        final CharGroup newNode = new CharGroup(currentGroup.mChars,
+                                bigrams, frequency, currentGroup.mChildren);
+                        currentNode.mData.set(nodeIndex, newNode);
+                        checkStack(currentNode);
+                    }
+                } else {
+                    // The new word matches the full old word and extends past it.
+                    // We only have to create a new node and add it to the end of this.
+                    final CharGroup newNode = new CharGroup(
+                            Arrays.copyOfRange(word, charIndex + differentCharIndex, word.length),
+                                    bigrams, frequency);
+                    currentGroup.mChildren = new Node();
+                    currentGroup.mChildren.mData.add(newNode);
+                }
+            } else {
+                if (0 == differentCharIndex) {
+                    // Exact same word. Check the frequency is 0 or -1, and update.
+                    if (0 != frequency) {
+                        if (0 < currentGroup.mFrequency) {
+                            throw new RuntimeException("This word already exists with frequency "
+                                    + currentGroup.mFrequency + " : "
+                                    + new String(word, 0, word.length));
+                        }
+                        final CharGroup newGroup = new CharGroup(word,
+                                currentGroup.mBigrams, frequency);
+                        currentNode.mData.set(nodeIndex, newGroup);
+                    }
+                } else {
+                    // Partial prefix match only. We have to replace the current node with a node
+                    // containing the current prefix and create two new ones for the tails.
+                    Node newChildren = new Node();
+                    final CharGroup newOldWord = new CharGroup(
+                            Arrays.copyOfRange(currentGroup.mChars, differentCharIndex,
+                                    currentGroup.mChars.length),
+                            currentGroup.mBigrams, currentGroup.mFrequency, currentGroup.mChildren);
+                    newChildren.mData.add(newOldWord);
+
+                    final CharGroup newParent;
+                    if (charIndex + differentCharIndex >= word.length) {
+                        newParent = new CharGroup(
+                                Arrays.copyOfRange(currentGroup.mChars, 0, differentCharIndex),
+                                        bigrams, frequency, newChildren);
+                    } else {
+                        newParent = new CharGroup(
+                                Arrays.copyOfRange(currentGroup.mChars, 0, differentCharIndex),
+                                        null, -1, newChildren);
+                        final CharGroup newWord = new CharGroup(
+                                Arrays.copyOfRange(word, charIndex + differentCharIndex,
+                                        word.length), bigrams, frequency);
+                        final int addIndex = word[charIndex + differentCharIndex]
+                                > currentGroup.mChars[differentCharIndex] ? 1 : 0;
+                        newChildren.mData.add(addIndex, newWord);
+                    }
+                    currentNode.mData.set(nodeIndex, newParent);
+                }
+                checkStack(currentNode);
+            }
+        }
+    }
+
+    /**
+     * Custom comparison of two int arrays taken to contain character codes.
+     *
+     * This method compares the two arrays passed as an argument in a lexicographic way,
+     * with an offset in the dst string.
+     * This method does NOT test for the first character. It is taken to be equal.
+     * I repeat: this method starts the comparison at 1 <> dstOffset + 1.
+     * The index where the strings differ is returned. ARRAYS_ARE_EQUAL = 0 is returned if the
+     * strings are equal. This works BECAUSE we don't look at the first character.
+     *
+     * @param src the left-hand side string of the comparison.
+     * @param dst the right-hand side string of the comparison.
+     * @param dstOffset the offset in the right-hand side string.
+     * @return the index at which the strings differ, or ARRAYS_ARE_EQUAL = 0 if they don't.
+     */
+    private static int ARRAYS_ARE_EQUAL = 0;
+    private static int compareArrays(final int[] src, final int[] dst, int dstOffset) {
+        // We do NOT test the first char, because we come from a method that already
+        // tested it.
+        for (int i = 1; i < src.length; ++i) {
+            if (dstOffset + i >= dst.length) return i;
+            if (src[i] != dst[dstOffset + i]) return i;
+        }
+        if (dst.length > src.length) return src.length;
+        return ARRAYS_ARE_EQUAL;
+    }
+
+    /**
+     * Helper class that compares and sorts two chargroups according to their
+     * first element only. I repeat: ONLY the first element is considered, the rest
+     * is ignored.
+     * This comparator imposes orderings that are inconsistent with equals.
+     */
+    static private class CharGroupComparator implements java.util.Comparator {
+        public int compare(Object o1, Object o2) {
+            final CharGroup c1 = (CharGroup)o1;
+            final CharGroup c2 = (CharGroup)o2;
+            if (c1.mChars[0] == c2.mChars[0]) return 0;
+            return c1.mChars[0] < c2.mChars[0] ? -1 : 1;
+        }
+        public boolean equals(Object o) {
+            return o instanceof CharGroupComparator;
+        }
+    }
+    final static private CharGroupComparator CHARGROUP_COMPARATOR = new CharGroupComparator();
+
+    /**
+     * Finds the insertion index of a character within a node.
+     */
+    private static int findInsertionIndex(final Node node, int character) {
+        final List data = node.mData;
+        final CharGroup reference = new CharGroup(new int[] { character }, null, 0);
+        int result = Collections.binarySearch(data, reference, CHARGROUP_COMPARATOR);
+        return result >= 0 ? result : -result - 1;
+    }
+
+    /**
+     * Find the index of a char in a node, if it exists.
+     *
+     * @param node the node to search in.
+     * @param character the character to search for.
+     * @return the position of the character if it's there, or CHARACTER_NOT_FOUND = -1 else.
+     */
+    private static int CHARACTER_NOT_FOUND = -1;
+    private static int findIndexOfChar(final Node node, int character) {
+        final int insertionIndex = findInsertionIndex(node, character);
+        if (node.mData.size() <= insertionIndex) return CHARACTER_NOT_FOUND;
+        return character == node.mData.get(insertionIndex).mChars[0] ? insertionIndex
+                : CHARACTER_NOT_FOUND;
+    }
+
+    /**
+     * Helper method to find a word in a given branch.
+     */
+    public static CharGroup findWordInTree(Node node, final String s) {
+        int index = 0;
+        final StringBuilder checker = new StringBuilder();
+
+        CharGroup currentGroup;
+        do {
+            int indexOfGroup = findIndexOfChar(node, s.codePointAt(index));
+            if (CHARACTER_NOT_FOUND == indexOfGroup) return null;
+            currentGroup = node.mData.get(indexOfGroup);
+            checker.append(new String(currentGroup.mChars, 0, currentGroup.mChars.length));
+            index += currentGroup.mChars.length;
+            if (index < s.length()) {
+                node = currentGroup.mChildren;
+            }
+        } while (null != node && index < s.length());
+
+        if (!s.equals(checker.toString())) return null;
+        return currentGroup;
+    }
+
+    /**
+     * Recursively count the number of character groups in a given branch of the trie.
+     *
+     * @param node the parent node.
+     * @return the number of char groups in all the branch under this node.
+     */
+    public static int countCharGroups(final Node node) {
+        final int nodeSize = node.mData.size();
+        int size = nodeSize;
+        for (int i = nodeSize - 1; i >= 0; --i) {
+            CharGroup group = node.mData.get(i);
+            if (null != group.mChildren)
+                size += countCharGroups(group.mChildren);
+        }
+        return size;
+    }
+
+    /**
+     * Recursively count the number of nodes in a given branch of the trie.
+     *
+     * @param node the node to count.
+     * @result the number of nodes in this branch.
+     */
+    public static int countNodes(final Node node) {
+        int size = 1;
+        for (int i = node.mData.size() - 1; i >= 0; --i) {
+            CharGroup group = node.mData.get(i);
+            if (null != group.mChildren)
+                size += countNodes(group.mChildren);
+        }
+        return size;
+    }
+
+    // Historically, the tails of the words were going to be merged to save space.
+    // However, that would prevent the code to search for a specific address in log(n)
+    // time so this was abandoned.
+    // The code is still of interest as it does add some compression to any dictionary
+    // that has no need for attributes. Implementations that does not read attributes should be
+    // able to read a dictionary with merged tails.
+    // Also, the following code does support frequencies, as in, it will only merges
+    // tails that share the same frequency. Though it would result in the above loss of
+    // performance while searching by address, it is still technically possible to merge
+    // tails that contain attributes, but this code does not take that into account - it does
+    // not compare attributes and will merge terminals with different attributes regardless.
+    public void mergeTails() {
+        MakedictLog.i("Do not merge tails");
+        return;
+
+//        MakedictLog.i("Merging nodes. Number of nodes : " + countNodes(root));
+//        MakedictLog.i("Number of groups : " + countCharGroups(root));
+//
+//        final HashMap<String, ArrayList<Node>> repository =
+//                  new HashMap<String, ArrayList<Node>>();
+//        mergeTailsInner(repository, root);
+//
+//        MakedictLog.i("Number of different pseudohashes : " + repository.size());
+//        int size = 0;
+//        for (ArrayList<Node> a : repository.values()) {
+//            size += a.size();
+//        }
+//        MakedictLog.i("Number of nodes after merge : " + (1 + size));
+//        MakedictLog.i("Recursively seen nodes : " + countNodes(root));
+    }
+
+    // The following methods are used by the deactivated mergeTails()
+//   private static boolean isEqual(Node a, Node b) {
+//       if (null == a && null == b) return true;
+//       if (null == a || null == b) return false;
+//       if (a.data.size() != b.data.size()) return false;
+//       final int size = a.data.size();
+//       for (int i = size - 1; i >= 0; --i) {
+//           CharGroup aGroup = a.data.get(i);
+//           CharGroup bGroup = b.data.get(i);
+//           if (aGroup.frequency != bGroup.frequency) return false;
+//           if (aGroup.alternates == null && bGroup.alternates != null) return false;
+//           if (aGroup.alternates != null && !aGroup.equals(bGroup.alternates)) return false;
+//           if (!Arrays.equals(aGroup.chars, bGroup.chars)) return false;
+//           if (!isEqual(aGroup.children, bGroup.children)) return false;
+//       }
+//       return true;
+//   }
+
+//   static private HashMap<String, ArrayList<Node>> mergeTailsInner(
+//           final HashMap<String, ArrayList<Node>> map, final Node node) {
+//       final ArrayList<CharGroup> branches = node.data;
+//       final int nodeSize = branches.size();
+//       for (int i = 0; i < nodeSize; ++i) {
+//           CharGroup group = branches.get(i);
+//           if (null != group.children) {
+//               String pseudoHash = getPseudoHash(group.children);
+//               ArrayList<Node> similarList = map.get(pseudoHash);
+//               if (null == similarList) {
+//                   similarList = new ArrayList<Node>();
+//                   map.put(pseudoHash, similarList);
+//               }
+//               boolean merged = false;
+//               for (Node similar : similarList) {
+//                   if (isEqual(group.children, similar)) {
+//                       group.children = similar;
+//                       merged = true;
+//                       break;
+//                   }
+//               }
+//               if (!merged) {
+//                   similarList.add(group.children);
+//               }
+//               mergeTailsInner(map, group.children);
+//           }
+//       }
+//       return map;
+//   }
+
+//  private static String getPseudoHash(final Node node) {
+//      StringBuilder s = new StringBuilder();
+//      for (CharGroup g : node.data) {
+//          s.append(g.frequency);
+//          for (int ch : g.chars){
+//              s.append(Character.toChars(ch));
+//          }
+//      }
+//      return s.toString();
+//  }
+
+    /**
+     * Iterator to walk through a dictionary.
+     *
+     * This is purely for convenience.
+     */
+    public static class DictionaryIterator implements Iterator<Word> {
+
+        private static class Position {
+            public Iterator<CharGroup> pos;
+            public int length;
+            public Position(ArrayList<CharGroup> groups) {
+                pos = groups.iterator();
+                length = 0;
+            }
+        }
+        final StringBuilder mCurrentString;
+        final LinkedList<Position> mPositions;
+
+        public DictionaryIterator(ArrayList<CharGroup> root) {
+            mCurrentString = new StringBuilder();
+            mPositions = new LinkedList<Position>();
+            final Position rootPos = new Position(root);
+            mPositions.add(rootPos);
+        }
+
+        @Override
+        public boolean hasNext() {
+            for (Position p : mPositions) {
+                if (p.pos.hasNext()) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public Word next() {
+            Position currentPos = mPositions.getLast();
+            mCurrentString.setLength(mCurrentString.length() - currentPos.length);
+
+            do {
+                if (currentPos.pos.hasNext()) {
+                    final CharGroup currentGroup = currentPos.pos.next();
+                    currentPos.length = currentGroup.mChars.length;
+                    for (int i : currentGroup.mChars)
+                        mCurrentString.append(Character.toChars(i));
+                    if (null != currentGroup.mChildren) {
+                        currentPos = new Position(currentGroup.mChildren.mData);
+                        mPositions.addLast(currentPos);
+                    }
+                    if (currentGroup.mFrequency >= 0)
+                        return new Word(mCurrentString.toString(), currentGroup.mFrequency,
+                                currentGroup.mBigrams);
+                } else {
+                    mPositions.removeLast();
+                    currentPos = mPositions.getLast();
+                    mCurrentString.setLength(mCurrentString.length() - mPositions.getLast().length);
+                }
+            } while(true);
+        }
+
+        @Override
+        public void remove() {
+            throw new UnsupportedOperationException("Unsupported yet");
+        }
+
+    }
+
+    /**
+     * Method to return an iterator.
+     *
+     * This method enables Java's enhanced for loop. With this you can have a FusionDictionary x
+     * and say : for (Word w : x) {}
+     */
+    @Override
+    public Iterator<Word> iterator() {
+        return new DictionaryIterator(mRoot.mData);
+    }
+}
diff --git a/tools/makedict2/src/com/android/inputmethod/latin/MakedictLog.java b/tools/makedict2/src/com/android/inputmethod/latin/MakedictLog.java
new file mode 100644
index 0000000000000000000000000000000000000000..badb2ff3ddfc514f7fc854665781b9ec4ec600b1
--- /dev/null
+++ b/tools/makedict2/src/com/android/inputmethod/latin/MakedictLog.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package com.android.inputmethod.latin;
+
+/**
+ * Wrapper to redirect log events to the right output medium.
+ */
+public class MakedictLog {
+
+    private static void print(String message) {
+        System.out.println(message);
+    }
+
+    public static void d(String message) {
+        print(message);
+    }
+    public static void e(String message) {
+        print(message);
+    }
+    public static void i(String message) {
+        print(message);
+    }
+    public static void w(String message) {
+        print(message);
+    }
+}
diff --git a/tools/makedict2/src/com/android/inputmethod/latin/PendingAttribute.java b/tools/makedict2/src/com/android/inputmethod/latin/PendingAttribute.java
new file mode 100644
index 0000000000000000000000000000000000000000..e5020218458e56fa77390e9851a5ab75052575a7
--- /dev/null
+++ b/tools/makedict2/src/com/android/inputmethod/latin/PendingAttribute.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package com.android.inputmethod.latin;
+
+/**
+ * A not-yet-resolved attribute.
+ *
+ * An attribute is either a bigram or an shortcut.
+ * All instances of this class are always immutable.
+ */
+public class PendingAttribute {
+    public final int mFrequency;
+    public final int mAddress;
+    public PendingAttribute(final int frequency, final int address) {
+        mFrequency = frequency;
+        mAddress = address;
+    }
+}
diff --git a/tools/makedict2/src/com/android/inputmethod/latin/UnsupportedFormatException.java b/tools/makedict2/src/com/android/inputmethod/latin/UnsupportedFormatException.java
new file mode 100644
index 0000000000000000000000000000000000000000..511ad5675096fccf2af9804b2ecbb60064b3c5c5
--- /dev/null
+++ b/tools/makedict2/src/com/android/inputmethod/latin/UnsupportedFormatException.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package com.android.inputmethod.latin;
+
+/**
+ * Simple exception thrown when a file format is not recognized.
+ */
+public class UnsupportedFormatException extends Exception {
+    public UnsupportedFormatException(String description) {
+        super(description);
+    }
+}
diff --git a/tools/makedict2/src/com/android/inputmethod/latin/Word.java b/tools/makedict2/src/com/android/inputmethod/latin/Word.java
new file mode 100644
index 0000000000000000000000000000000000000000..916165a419fd6ba1874b68461aa6884388b785e0
--- /dev/null
+++ b/tools/makedict2/src/com/android/inputmethod/latin/Word.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package com.android.inputmethod.latin;
+
+import com.android.inputmethod.latin.FusionDictionary.WeightedString;
+
+import java.util.ArrayList;
+
+/**
+ * Utility class for a word with a frequency.
+ *
+ * This is chiefly used to iterate a dictionary.
+ */
+public class Word implements Comparable<Word> {
+    final String mWord;
+    final int mFrequency;
+    final ArrayList<WeightedString> mBigrams;
+
+    public Word(String word, int frequency, ArrayList<WeightedString> bigrams) {
+        mWord = word;
+        mFrequency = frequency;
+        mBigrams = bigrams;
+    }
+
+    /**
+     * Three-way comparison.
+     *
+     * A Word x is greater than a word y if x has a higher frequency. If they have the same
+     * frequency, they are sorted in lexicographic order.
+     */
+    @Override
+    public int compareTo(Word w) {
+        if (mFrequency < w.mFrequency) return 1;
+        if (mFrequency > w.mFrequency) return -1;
+        return mWord.compareTo(w.mWord);
+    }
+
+    /**
+     * Equality test.
+     *
+     * Words are equal if they have the same frequency, the same spellings, and the same
+     * attributes.
+     */
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof Word)) return false;
+        Word w = (Word)o;
+        return mFrequency == w.mFrequency && mWord.equals(w.mWord)
+                && mBigrams.equals(w.mBigrams);
+    }
+}
diff --git a/tools/makedict2/src/com/android/inputmethod/latin/XmlDictInputOutput.java b/tools/makedict2/src/com/android/inputmethod/latin/XmlDictInputOutput.java
new file mode 100644
index 0000000000000000000000000000000000000000..096bfd1825d27298264644ccca01a93a87a0672e
--- /dev/null
+++ b/tools/makedict2/src/com/android/inputmethod/latin/XmlDictInputOutput.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package com.android.inputmethod.latin;
+
+import com.android.inputmethod.latin.FusionDictionary.WeightedString;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.TreeSet;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * Reads and writes XML files for a FusionDictionary.
+ *
+ * All functions in this class are static.
+ */
+public class XmlDictInputOutput {
+
+    private static final String WORD_TAG = "w";
+    private static final String BIGRAM_TAG = "bigram";
+    private static final String FREQUENCY_ATTR = "f";
+    private static final String WORD_ATTR = "word";
+
+    /**
+     * SAX handler for a unigram XML file.
+     */
+    static private class UnigramHandler extends DefaultHandler {
+        // Parser states
+        private static final int NONE = 0;
+        private static final int START = 1;
+        private static final int WORD = 2;
+        private static final int BIGRAM = 4;
+        private static final int END = 5;
+        private static final int UNKNOWN = 6;
+
+        final FusionDictionary mDictionary;
+        int mState; // the state of the parser
+        int mFreq; // the currently read freq
+        final HashMap<String, ArrayList<WeightedString>> mBigramsMap;
+
+        /**
+         * Create the handler.
+         *
+         * @param dict the dictionary to construct.
+         * @param bigrams the bigrams as a map. This may be empty, but may not be null.
+         */
+        public UnigramHandler(FusionDictionary dict,
+                HashMap<String, ArrayList<WeightedString>> bigrams) {
+            mDictionary = dict;
+            mBigramsMap = bigrams;
+            mState = START;
+            mFreq = 0;
+        }
+
+        @Override
+        public void startElement(String uri, String localName, String qName, Attributes attrs) {
+            if (WORD_TAG.equals(localName)) {
+                mState = WORD;
+                for (int attrIndex = 0; attrIndex < attrs.getLength(); ++attrIndex) {
+                    final String attrName = attrs.getLocalName(attrIndex);
+                    if (FREQUENCY_ATTR.equals(attrName)) {
+                        mFreq = Integer.parseInt(attrs.getValue(attrIndex));
+                    }
+                }
+            } else {
+                mState = UNKNOWN;
+            }
+        }
+
+        @Override
+        public void characters(char[] ch, int start, int length) {
+            if (WORD == mState) {
+                final String word = String.copyValueOf(ch, start, length);
+                mDictionary.add(word, mFreq, mBigramsMap.get(word));
+            }
+        }
+
+        @Override
+        public void endElement(String uri, String localName, String qName) {
+            if (WORD == mState) mState = START;
+        }
+    }
+
+    /**
+     * SAX handler for a bigram XML file.
+     */
+    static private class BigramHandler extends DefaultHandler {
+        private final static String BIGRAM_W1_TAG = "bi";
+        private final static String BIGRAM_W2_TAG = "w";
+        private final static String BIGRAM_W1_ATTRIBUTE = "w1";
+        private final static String BIGRAM_W2_ATTRIBUTE = "w2";
+        private final static String BIGRAM_FREQ_ATTRIBUTE = "p";
+
+        String mW1;
+        final HashMap<String, ArrayList<WeightedString>> mBigramsMap;
+
+        public BigramHandler() {
+            mW1 = null;
+            mBigramsMap = new HashMap<String, ArrayList<WeightedString>>();
+        }
+
+        @Override
+        public void startElement(String uri, String localName, String qName, Attributes attrs) {
+            if (BIGRAM_W1_TAG.equals(localName)) {
+                mW1 = attrs.getValue(uri, BIGRAM_W1_ATTRIBUTE);
+            } else if (BIGRAM_W2_TAG.equals(localName)) {
+                String w2 = attrs.getValue(uri, BIGRAM_W2_ATTRIBUTE);
+                int freq = Integer.parseInt(attrs.getValue(uri, BIGRAM_FREQ_ATTRIBUTE));
+                WeightedString bigram = new WeightedString(w2, freq / 8);
+                ArrayList<WeightedString> bigramList = mBigramsMap.get(mW1);
+                if (null == bigramList) bigramList = new ArrayList<WeightedString>();
+                bigramList.add(bigram);
+                mBigramsMap.put(mW1, bigramList);
+            }
+        }
+
+        public HashMap<String, ArrayList<WeightedString>> getBigramMap() {
+            return mBigramsMap;
+        }
+    }
+
+    /**
+     * Reads a dictionary from an XML file.
+     *
+     * This is the public method that will parse an XML file and return the corresponding memory
+     * representation.
+     *
+     * @param unigrams the file to read the data from.
+     * @return the in-memory representation of the dictionary.
+     */
+    public static FusionDictionary readDictionaryXml(InputStream unigrams, InputStream bigrams)
+            throws SAXException, IOException, ParserConfigurationException {
+        final SAXParserFactory factory = SAXParserFactory.newInstance();
+        factory.setNamespaceAware(true);
+        final SAXParser parser = factory.newSAXParser();
+        final BigramHandler bigramHandler = new BigramHandler();
+        if (null != bigrams) parser.parse(bigrams, bigramHandler);
+
+        final FusionDictionary dict = new FusionDictionary();
+        final UnigramHandler unigramHandler =
+                new UnigramHandler(dict, bigramHandler.getBigramMap());
+        parser.parse(unigrams, unigramHandler);
+        return dict;
+    }
+
+    /**
+     * Reads a dictionary in the first, legacy XML format
+     *
+     * This method reads data from the parser and creates a new FusionDictionary with it.
+     * The format parsed by this method is the format used before Ice Cream Sandwich,
+     * which has no support for bigrams or shortcuts.
+     * It is important to note that this method expects the parser to have already eaten
+     * the first, all-encompassing tag.
+     *
+     * @param xpp the parser to read the data from.
+     * @return the parsed dictionary.
+     */
+
+    /**
+     * Writes a dictionary to an XML file.
+     *
+     * The output format is the "second" format, which supports bigrams and shortcuts.
+     *
+     * @param destination a destination stream to write to.
+     * @param dict the dictionary to write.
+     */
+    public static void writeDictionaryXml(Writer destination, FusionDictionary dict)
+            throws IOException {
+        final TreeSet<Word> set = new TreeSet<Word>();
+        for (Word word : dict) {
+            set.add(word);
+        }
+        // TODO: use an XMLSerializer if this gets big
+        destination.write("<wordlist format=\"2\">\n");
+        for (Word word : set) {
+            destination.write("  <" + WORD_TAG + " " + WORD_ATTR + "=\"" + word.mWord + "\" "
+                    + FREQUENCY_ATTR + "=\"" + word.mFrequency + "\">");
+            if (null != word.mBigrams) {
+                destination.write("\n");
+                for (WeightedString bigram : word.mBigrams) {
+                    destination.write("    <" + BIGRAM_TAG + " " + FREQUENCY_ATTR + "=\""
+                            + bigram.mFrequency + "\">" + bigram.mWord + "</" + BIGRAM_TAG + ">\n");
+                }
+                destination.write("  ");
+            }
+            destination.write("</" + WORD_TAG + ">\n");
+        }
+        destination.write("</wordlist>\n");
+        destination.close();
+    }
+}
diff --git a/tools/makedict2/tests/com/android/inputmethod/latin/BinaryDictInputOutputTest.java b/tools/makedict2/tests/com/android/inputmethod/latin/BinaryDictInputOutputTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..79cf14b2be4aaeade91a127595d631371777eefb
--- /dev/null
+++ b/tools/makedict2/tests/com/android/inputmethod/latin/BinaryDictInputOutputTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package com.android.inputmethod.latin;
+
+import com.android.inputmethod.latin.FusionDictionary.Node;
+
+import java.util.ArrayList;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for BinaryDictInputOutput.
+ */
+public class BinaryDictInputOutputTest extends TestCase {
+
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    // Test the flattened array contains the expected number of nodes, and
+    // that it does not contain any duplicates.
+    public void testFlattenNodes() {
+        final FusionDictionary dict = new FusionDictionary();
+        dict.add("foo", 1, null);
+        dict.add("fta", 1, null);
+        dict.add("ftb", 1, null);
+        dict.add("bar", 1, null);
+        dict.add("fool", 1, null);
+        final ArrayList<Node> result = BinaryDictInputOutput.flattenTree(dict.mRoot);
+        assertEquals(4, result.size());
+        while (!result.isEmpty()) {
+            final Node n = result.remove(0);
+            assertFalse("Flattened array contained the same node twice", result.contains(n));
+        }
+    }
+
+}