{"article":{"slug":"how-we-made-our-mmdb-files-a-third-smaller-without-changing-a-byte-of-data","title":"How We Made Our MMDB Files a Third Smaller Without Changing a Byte of Data","subtitle":null,"summary":"IPinfo's Tiago Martins and Silvano Cerza explain how hash-consing the MMDB search tree, exploiting how repetitive internet address data is, shrank IPinfo's 89 daily databases from 91.2 GB to 62.8 GB (31%, up to 80% for ipinfo_privacy) with no compression and no reader changes, how they proved the output is the same database, and why the mmdbshrink tool and mmdbctl update are open source.","content_type":"blog_post","language":"en","canonical_url":"https://ipinfo.io/blog/how-made-mmdb-files-smaller","author":{"name":"Tiago Martins and Silvano Cerza","url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"IPinfo","url":"https://ipinfo.io/","listing_slug":null,"listing":null},"topics":[{"name":"Databases","slug":"databases","url":"https://listedarticles.com/topics/databases"},{"name":"Data Engineering","slug":"data-engineering","url":"https://listedarticles.com/topics/data-engineering"},{"name":"Open Source","slug":"open-source","url":"https://listedarticles.com/topics/open-source"},{"name":"Networking","slug":"networking","url":"https://listedarticles.com/topics/networking"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":1588,"reading_minutes":7,"published_at":"2026-10-08T00:00:00.000Z","added_at":"2026-10-29T02:07:41.603Z","updated_at":"2026-10-29T02:07:41.603Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/how-we-made-our-mmdb-files-a-third-smaller-without-changing-a-byte-of-data","markdown_url":"https://listedarticles.com/articles/how-we-made-our-mmdb-files-a-third-smaller-without-changing-a-byte-of-data.md","example":false,"citation":"Tiago Martins and Silvano Cerza, IPinfo. \"How We Made Our MMDB Files a Third Smaller Without Changing a Byte of Data.\" 8 Oct 2026. https://ipinfo.io/blog/how-made-mmdb-files-smaller (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://ipinfo.io/blog/how-made-mmdb-files-smaller"},"body_markdown":"*Tiago Martins & Silvano Cerza · IPinfo · October 8, 2026*\n\nIn August we told database customers their [files had shrunk by up to 80%](https://ipinfo.io/blog/ipinfo-smaller-databases), and promised to explain how. This is that explanation. The tool that does it is now open source.\n\n## What We Did\n\nWe found a serious size saving in the MMDB file format, described below. We shipped it as an update to mmdbctl and as a standalone tool, mmdbshrink. The tool takes an existing MMDB file and writes a smaller one. The new file is a standard MMDB: every reader library that opens the old file opens the new one and returns the same answer for every IP address. Across the 89 databases we build every day, the catalogue went from 91.2 GB to 62.8 GB, a 31% reduction. The best single result was ipinfo\\_privacy, from 789 MB to 155 MB, an 80% reduction.\n\nNothing was compressed. There is no decompression step, no new reader, no new format. The saving is real at rest, on the wire, and in memory, because MMDB files are memory-mapped and the file size is the memory footprint.\n\nThis tool is available in [https://github.com/ipinfo/mmdbctl/releases/latest](https://github.com/ipinfo/mmdbctl/releases/latest?ref=ipinfo.io)**.**\n\n## How an MMDB File Is Laid Out\n\nAn MMDB file has two parts. The **search tree** is a binary trie over the bits of an IP address. Start at the root, read the address one bit at a time, go left on 0 and right on 1, and stop when you reach a leaf. The leaf holds a pointer into the second part, the **data section**, where the records live: country, city, ASN, privacy flags, whatever the database carries.\n\nThe format's designers took care over the data section. It has a pointer type, so a record that repeats can be written once and referenced from many leaves, and the writers in common use do this.\n\n![](https://storage.ghost.io/c/b1/57/b157bf7f-0fe2-447f-a945-12d7951755b3/content/images/2026/10/how-mmdb-file-laid-out.png)\n\nThat is why, in the ipinfo\\_lite run in the worked example further down, the data section does not change size at all.\n\nThe search tree got less attention. Every node is written out as two records, one per child, and a standard writer emits every node it visits. It never asks whether it has already written an identical node.\n\n## Why That Matters: The Internet Is Repetitive\n\nConsider two /24 networks in different parts of the address space that carry the same answer for every one of their 256 addresses. Below the /24 boundary, the subtrees under those two prefixes are identical, bit for bit: the same shape, the same leaves, the same pointers into the data section. A standard writer stores both.\n\nNow scale that up. A privacy database is a small set of verdicts applied to millions of ranges. A location database has a few hundred thousand distinct city records spread over millions of prefixes. Wherever two prefixes resolve to the same record, and the same is true of everything beneath them, their subtrees are duplicates. In our files, a large share of the tree was duplicate subtrees.\n\n## The Fix: Hash-Consing the Tree\n\nHash-consing is a long-established technique from compiler and symbolic-computation work, described by [Filliâtre and Conchon (2006), *Type-Safe Modular Hash-Consing*](https://doi.org/10.1145/1159876.1159880?ref=ipinfo.io), and closely related to the subgraph sharing in [Bryant’s binary decision diagrams (1986)](https://doi.org/10.1109/TC.1986.1676819?ref=ipinfo.io). The idea is simple: keep a hash table of nodes already created, and reuse an existing node whenever its contents are structurally identical to the one you are about to create. Applied bottom-up to a trie, this means processing the children first, then sharing nodes with the same pair of children. The result is a directed acyclic graph: multiple parents can point to the same subtree, which is stored only once.\n\nApplied to an MMDB search tree, the procedure is:\n\n1. Traverse the existing tree from the leaves upward, processing children before their parents.\n2. For each node, replace references to child nodes with their canonical identities. References to data records and empty results retain their meaning.\n3. Use the ordered pair of child references as the key in a hash table. If that exact pair is already present, reuse the canonical node. Otherwise, add a new canonical node. The table checks the complete pair for equality, so hash collisions never cause a merge.\n4. Write out the canonical nodes, keeping the root at node zero, renumbering node references, and updating `node_count` and the encoded data references.\n\nThis requires no change to the MMDB format. Each node contains two records, which can identify another node, a data record, or an empty result. Multiple parents can refer to the same node; the [MMDB format specification](https://maxmind.github.io/MaxMind-DB/?ref=ipinfo.io#binary-search-tree-section) already describes this kind of sharing for IPv4 aliases.\n\nSharing identical subtrees preserves the sequence of branch decisions for each IP address and leads to the same data record or empty result. The file’s layout and size change; its lookup results stay the same.\n\n![](https://storage.ghost.io/c/b1/57/b157bf7f-0fe2-447f-a945-12d7951755b3/content/images/2026/10/mmdb-layout-types.png)\n\nWe were the first to identify and ship this saving. When we started, the reference writer emitted a fresh node per position, and so did the other writers we looked at. A conceptually similar technique appeared last year in a WhatsApp vulnerability paper, where the researchers needed to store a very large set of phone numbers and built their own custom structure to do it. Ours differs in one respect: the output is still a valid MMDB.\n\n## The Numbers\n\nAcross our production catalogue:\n\n|  |  |  |  |\n| --- | --- | --- | --- |\n| Database | Before | After | Reduction |\n| ipinfo\\_privacy | 789 MB | 155 MB | 80% |\n| ipinfo\\_location | 687 MB | 316 MB | 54% |\n| ipinfo\\_core | 1.5 GB | 728 MB | 53% |\n| ipinfo\\_lite | 38 MB | 25 MB | 34% |\n| ipinfo\\_plus | 5.6 GB | 3.8 GB | 32% |\n| resproxy | 3.5 GB | 2.6 GB | 27% |\n| Total: 89 databases | 91.2 GB | 62.8 GB | 31% |\n\nLookup speed is unchanged or slightly better. In the ipinfo\\_lite benchmark below, one million lookups ran at 797 ns per operation on the original and 757 ns on the optimized file, and resident memory fell from 36.02 MiB to 23.92 MiB. Fewer tree bytes means fewer page faults.\n\n## A Worked Example\n\nThis is the tool run against our free Lite database, exactly as it appears in the README.\n\n```\n$ mmdbshrink --full-report ipinfo_lite.mmdb\n\ninput:   ipinfo_lite.mmdb\noutput:  ipinfo_lite.shrunk.mmdb\n\n  size:   37.76 MB   ->  25.08 MB   (saved 12.68 MB, -33.58%)\n  nodes:  3,790,310  ->  2,205,358  (-41.82%)\n  tree:   30.32 MB   ->  17.64 MB\n  data:   7.44 MB    ->  7.44 MB    (unchanged)\n\nelapsed: 452ms\n\nvalidation (seed 1):\n  [OK] metadata        ip_version=6  record_size=32  binary_format=2.0\n                       database_type: ipinfo bundle_location_lite.mmdb  build_epoch=1790582633\n                       node_count: 3,790,310 -> 2,205,358 (-41.82%)\n  [OK] fixed probes    n=39  hits=7  (0s)\n  [OK] random IPv4     n=1,000,000  hits=861,887 (86.19%)  mismatches=0  (1.73s)\n  [OK] random IPv6     n=1,000,000  hits=1,985 (0.20%)  mismatches=0  (39ms)\n\nbench (1,000,000 lookups each):\n  input:    size=37.76 MB    per_op= 797ns  qps=1,320,636\n  output:   size=25.08 MB    per_op= 757ns  qps=1,319,347\n\nmemory (200,000 lookups each, separate processes):\n  input:    mmap_cached=36.02 MiB   proc_rss=29.34 MiB   per_op= 707ns  minflt=155  majflt=0\n  output:   mmap_cached=23.92 MiB   proc_rss=27.58 MiB   per_op= 695ns  minflt=168  majflt=0\n```\n\nRead the last three lines together. The data section is untouched. Every byte saved came out of the search tree, and 41% of the nodes turned out to be duplicates of nodes already written.\n\n## Proving the Output Is the Same Database\n\nA smaller file is worth nothing if a single lookup changes. Before any optimized file reached a customer we checked it four ways.\n\n1. **Full enumeration in both directions.** Every prefix in the original is looked up in the optimized file and vice versa. For ipinfo\\_lite that is 3,783,922 prefixes, 7,567,844 lookups, all matching.\n2. **Random probes.** One million random IPv4 and one million random IPv6 addresses, zero mismatches.\n3. **Existing tooling.** *mmdbctl verify* reports the file valid, *mmdbctl diff* reports 0 subnets and 0 records modified, and MaxMind's own mmdbverify passes. We tested the files against 12 MMDB reader SDKs.\n4. **Benchmark parity** on latency and memory, as above.\n\nThe first optimized files went into production in July, first on a customer's custom dataset that had grown past a 2 GB platform limit, then into our own API, where the smaller files cut the memory each instance needs. Today they are the default: 100% of API traffic and 100% of IPinfo database downloads serve optimized files under the same filenames as before.\n\n## Why We Are Giving It Away\n\nMMDB is an open format that gives fast lookups of IP metadata, and the ecosystem supports it widely. We have published open source tooling for it before, and this continues that commitment. The duplication we found is a property of the format's writers, and anyone shipping MMDB files has the same tree bloat we had. What we compete on is what goes into the file. How the file is packed is not where our advantage lives, so the optimizer is open source under **Apache 2.0**.\n\nIf you build MMDB files, run it on yours. If you read MMDB files, you need do nothing at all.\n\nYou can find the project in this GitHub repository: [https://github.com/ipinfo/mmdbctl/tree/master/mmdbshrink](https://github.com/ipinfo/mmdbctl/tree/master/mmdbshrink?ref=ipinfo.io)\n\nYou can easily install directly with `go install github.com/ipinfo/mmdbctl/mmdbzip@latest`, for other options see the [Installation section in the README.md](https://github.com/ipinfo/mmdbctl/tree/master/mmdbshrink?ref=ipinfo.io#installation).\n\nAfter installing you’re ready to use it like this: `mmdbshrink ipinfo_lite.mmdb`\n\nIf you are an IPinfo database customer, you don't need to do anything. Every MMDB you download already has these savings applied.\n\n---\n\n*Originally published at [the IPinfo blog](https://ipinfo.io/blog/how-made-mmdb-files-smaller).*\n","body_html":"<p><em>Tiago Martins &amp; Silvano Cerza · IPinfo · October 8, 2026</em></p>\n<p>In August we told database customers their <a href=\"https://ipinfo.io/blog/ipinfo-smaller-databases\" rel=\"nofollow ugc noopener\">files had shrunk by up to 80%</a>, and promised to explain how. This is that explanation. The tool that does it is now open source.</p>\n<h2 id=\"what-we-did\">What We Did</h2>\n<p>We found a serious size saving in the MMDB file format, described below. We shipped it as an update to mmdbctl and as a standalone tool, mmdbshrink. The tool takes an existing MMDB file and writes a smaller one. The new file is a standard MMDB: every reader library that opens the old file opens the new one and returns the same answer for every IP address. Across the 89 databases we build every day, the catalogue went from 91.2 GB to 62.8 GB, a 31% reduction. The best single result was ipinfo_privacy, from 789 MB to 155 MB, an 80% reduction.</p>\n<p>Nothing was compressed. There is no decompression step, no new reader, no new format. The saving is real at rest, on the wire, and in memory, because MMDB files are memory-mapped and the file size is the memory footprint.</p>\n<p>This tool is available in <a href=\"https://github.com/ipinfo/mmdbctl/releases/latest?ref=ipinfo.io\" rel=\"nofollow ugc noopener\"><a href=\"https://github.com/ipinfo/mmdbctl/releases/latest\" rel=\"nofollow ugc noopener\">https://github.com/ipinfo/mmdbctl/releases/latest</a></a>*<em>.*</em></p>\n<h2 id=\"how-an-mmdb-file-is-laid-out\">How an MMDB File Is Laid Out</h2>\n<p>An MMDB file has two parts. The <strong>search tree</strong> is a binary trie over the bits of an IP address. Start at the root, read the address one bit at a time, go left on 0 and right on 1, and stop when you reach a leaf. The leaf holds a pointer into the second part, the <strong>data section</strong>, where the records live: country, city, ASN, privacy flags, whatever the database carries.</p>\n<p>The format&#39;s designers took care over the data section. It has a pointer type, so a record that repeats can be written once and referenced from many leaves, and the writers in common use do this.</p>\n<figure><img src=\"https://storage.ghost.io/c/b1/57/b157bf7f-0fe2-447f-a945-12d7951755b3/content/images/2026/10/how-mmdb-file-laid-out.png\" alt=\"\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>That is why, in the ipinfo_lite run in the worked example further down, the data section does not change size at all.</p>\n<p>The search tree got less attention. Every node is written out as two records, one per child, and a standard writer emits every node it visits. It never asks whether it has already written an identical node.</p>\n<h2 id=\"why-that-matters-the-internet-is-repetitive\">Why That Matters: The Internet Is Repetitive</h2>\n<p>Consider two /24 networks in different parts of the address space that carry the same answer for every one of their 256 addresses. Below the /24 boundary, the subtrees under those two prefixes are identical, bit for bit: the same shape, the same leaves, the same pointers into the data section. A standard writer stores both.</p>\n<p>Now scale that up. A privacy database is a small set of verdicts applied to millions of ranges. A location database has a few hundred thousand distinct city records spread over millions of prefixes. Wherever two prefixes resolve to the same record, and the same is true of everything beneath them, their subtrees are duplicates. In our files, a large share of the tree was duplicate subtrees.</p>\n<h2 id=\"the-fix-hash-consing-the-tree\">The Fix: Hash-Consing the Tree</h2>\n<p>Hash-consing is a long-established technique from compiler and symbolic-computation work, described by <a href=\"https://doi.org/10.1145/1159876.1159880?ref=ipinfo.io\" rel=\"nofollow ugc noopener\">Filliâtre and Conchon (2006), <em>Type-Safe Modular Hash-Consing</em></a>, and closely related to the subgraph sharing in <a href=\"https://doi.org/10.1109/TC.1986.1676819?ref=ipinfo.io\" rel=\"nofollow ugc noopener\">Bryant’s binary decision diagrams (1986)</a>. The idea is simple: keep a hash table of nodes already created, and reuse an existing node whenever its contents are structurally identical to the one you are about to create. Applied bottom-up to a trie, this means processing the children first, then sharing nodes with the same pair of children. The result is a directed acyclic graph: multiple parents can point to the same subtree, which is stored only once.</p>\n<p>Applied to an MMDB search tree, the procedure is:</p>\n<ol><li>Traverse the existing tree from the leaves upward, processing children before their parents.</li><li>For each node, replace references to child nodes with their canonical identities. References to data records and empty results retain their meaning.</li><li>Use the ordered pair of child references as the key in a hash table. If that exact pair is already present, reuse the canonical node. Otherwise, add a new canonical node. The table checks the complete pair for equality, so hash collisions never cause a merge.</li><li>Write out the canonical nodes, keeping the root at node zero, renumbering node references, and updating <code>node_count</code> and the encoded data references.</li></ol>\n<p>This requires no change to the MMDB format. Each node contains two records, which can identify another node, a data record, or an empty result. Multiple parents can refer to the same node; the <a href=\"https://maxmind.github.io/MaxMind-DB/?ref=ipinfo.io#binary-search-tree-section\" rel=\"nofollow ugc noopener\">MMDB format specification</a> already describes this kind of sharing for IPv4 aliases.</p>\n<p>Sharing identical subtrees preserves the sequence of branch decisions for each IP address and leads to the same data record or empty result. The file’s layout and size change; its lookup results stay the same.</p>\n<figure><img src=\"https://storage.ghost.io/c/b1/57/b157bf7f-0fe2-447f-a945-12d7951755b3/content/images/2026/10/mmdb-layout-types.png\" alt=\"\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>We were the first to identify and ship this saving. When we started, the reference writer emitted a fresh node per position, and so did the other writers we looked at. A conceptually similar technique appeared last year in a WhatsApp vulnerability paper, where the researchers needed to store a very large set of phone numbers and built their own custom structure to do it. Ours differs in one respect: the output is still a valid MMDB.</p>\n<h2 id=\"the-numbers\">The Numbers</h2>\n<p>Across our production catalogue:</p>\n<div class=\"table-wrap\"><table><thead><tr><th></th><th></th><th></th><th></th></tr></thead><tbody><tr><td>Database</td><td>Before</td><td>After</td><td>Reduction</td></tr><tr><td>ipinfo_privacy</td><td>789 MB</td><td>155 MB</td><td>80%</td></tr><tr><td>ipinfo_location</td><td>687 MB</td><td>316 MB</td><td>54%</td></tr><tr><td>ipinfo_core</td><td>1.5 GB</td><td>728 MB</td><td>53%</td></tr><tr><td>ipinfo_lite</td><td>38 MB</td><td>25 MB</td><td>34%</td></tr><tr><td>ipinfo_plus</td><td>5.6 GB</td><td>3.8 GB</td><td>32%</td></tr><tr><td>resproxy</td><td>3.5 GB</td><td>2.6 GB</td><td>27%</td></tr><tr><td>Total: 89 databases</td><td>91.2 GB</td><td>62.8 GB</td><td>31%</td></tr></tbody></table></div>\n<p>Lookup speed is unchanged or slightly better. In the ipinfo_lite benchmark below, one million lookups ran at 797 ns per operation on the original and 757 ns on the optimized file, and resident memory fell from 36.02 MiB to 23.92 MiB. Fewer tree bytes means fewer page faults.</p>\n<h2 id=\"a-worked-example\">A Worked Example</h2>\n<p>This is the tool run against our free Lite database, exactly as it appears in the README.</p>\n<pre><code>$ mmdbshrink --full-report ipinfo_lite.mmdb\n\ninput:   ipinfo_lite.mmdb\noutput:  ipinfo_lite.shrunk.mmdb\n\n  size:   37.76 MB   -&gt;  25.08 MB   (saved 12.68 MB, -33.58%)\n  nodes:  3,790,310  -&gt;  2,205,358  (-41.82%)\n  tree:   30.32 MB   -&gt;  17.64 MB\n  data:   7.44 MB    -&gt;  7.44 MB    (unchanged)\n\nelapsed: 452ms\n\nvalidation (seed 1):\n  [OK] metadata        ip_version=6  record_size=32  binary_format=2.0\n                       database_type: ipinfo bundle_location_lite.mmdb  build_epoch=1790582633\n                       node_count: 3,790,310 -&gt; 2,205,358 (-41.82%)\n  [OK] fixed probes    n=39  hits=7  (0s)\n  [OK] random IPv4     n=1,000,000  hits=861,887 (86.19%)  mismatches=0  (1.73s)\n  [OK] random IPv6     n=1,000,000  hits=1,985 (0.20%)  mismatches=0  (39ms)\n\nbench (1,000,000 lookups each):\n  input:    size=37.76 MB    per_op= 797ns  qps=1,320,636\n  output:   size=25.08 MB    per_op= 757ns  qps=1,319,347\n\nmemory (200,000 lookups each, separate processes):\n  input:    mmap_cached=36.02 MiB   proc_rss=29.34 MiB   per_op= 707ns  minflt=155  majflt=0\n  output:   mmap_cached=23.92 MiB   proc_rss=27.58 MiB   per_op= 695ns  minflt=168  majflt=0</code></pre>\n<p>Read the last three lines together. The data section is untouched. Every byte saved came out of the search tree, and 41% of the nodes turned out to be duplicates of nodes already written.</p>\n<h2 id=\"proving-the-output-is-the-same-database\">Proving the Output Is the Same Database</h2>\n<p>A smaller file is worth nothing if a single lookup changes. Before any optimized file reached a customer we checked it four ways.</p>\n<ol><li><strong>Full enumeration in both directions.</strong> Every prefix in the original is looked up in the optimized file and vice versa. For ipinfo_lite that is 3,783,922 prefixes, 7,567,844 lookups, all matching.</li><li><strong>Random probes.</strong> One million random IPv4 and one million random IPv6 addresses, zero mismatches.</li><li><strong>Existing tooling.</strong> <em>mmdbctl verify</em> reports the file valid, <em>mmdbctl diff</em> reports 0 subnets and 0 records modified, and MaxMind&#39;s own mmdbverify passes. We tested the files against 12 MMDB reader SDKs.</li><li><strong>Benchmark parity</strong> on latency and memory, as above.</li></ol>\n<p>The first optimized files went into production in July, first on a customer&#39;s custom dataset that had grown past a 2 GB platform limit, then into our own API, where the smaller files cut the memory each instance needs. Today they are the default: 100% of API traffic and 100% of IPinfo database downloads serve optimized files under the same filenames as before.</p>\n<h2 id=\"why-we-are-giving-it-away\">Why We Are Giving It Away</h2>\n<p>MMDB is an open format that gives fast lookups of IP metadata, and the ecosystem supports it widely. We have published open source tooling for it before, and this continues that commitment. The duplication we found is a property of the format&#39;s writers, and anyone shipping MMDB files has the same tree bloat we had. What we compete on is what goes into the file. How the file is packed is not where our advantage lives, so the optimizer is open source under <strong>Apache 2.0</strong>.</p>\n<p>If you build MMDB files, run it on yours. If you read MMDB files, you need do nothing at all.</p>\n<p>You can find the project in this GitHub repository: <a href=\"https://github.com/ipinfo/mmdbctl/tree/master/mmdbshrink?ref=ipinfo.io\" rel=\"nofollow ugc noopener\"><a href=\"https://github.com/ipinfo/mmdbctl/tree/master/mmdbshrink\" rel=\"nofollow ugc noopener\">https://github.com/ipinfo/mmdbctl/tree/master/mmdbshrink</a></a></p>\n<p>You can easily install directly with <code>go install github.com/ipinfo/mmdbctl/mmdbzip@latest</code>, for other options see the <a href=\"https://github.com/ipinfo/mmdbctl/tree/master/mmdbshrink?ref=ipinfo.io#installation\" rel=\"nofollow ugc noopener\">Installation section in the README.md</a>.</p>\n<p>After installing you’re ready to use it like this: <code>mmdbshrink ipinfo_lite.mmdb</code></p>\n<p>If you are an IPinfo database customer, you don&#39;t need to do anything. Every MMDB you download already has these savings applied.</p>\n<hr />\n<p><em>Originally published at <a href=\"https://ipinfo.io/blog/how-made-mmdb-files-smaller\" rel=\"nofollow ugc noopener\">the IPinfo blog</a>.</em></p>","headings":[{"level":2,"text":"What We Did","id":"what-we-did"},{"level":2,"text":"How an MMDB File Is Laid Out","id":"how-an-mmdb-file-is-laid-out"},{"level":2,"text":"Why That Matters: The Internet Is Repetitive","id":"why-that-matters-the-internet-is-repetitive"},{"level":2,"text":"The Fix: Hash-Consing the Tree","id":"the-fix-hash-consing-the-tree"},{"level":2,"text":"The Numbers","id":"the-numbers"},{"level":2,"text":"A Worked Example","id":"a-worked-example"},{"level":2,"text":"Proving the Output Is the Same Database","id":"proving-the-output-is-the-same-database"},{"level":2,"text":"Why We Are Giving It Away","id":"why-we-are-giving-it-away"}]}}