You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sedona.apache.org by ji...@apache.org on 2026/07/24 15:39:14 UTC

(sedona) branch branch-1.9.0 updated: [DOCS] Add blog post: reading OSM PBF into Sedona (#3160)

This is an automated email from the ASF dual-hosted git repository.

jiayuasu pushed a commit to branch branch-1.9.0
in repository https://gitbox.apache.org/repos/asf/sedona.git


The following commit(s) were added to refs/heads/branch-1.9.0 by this push:
     new 02eb7b85c1d [DOCS] Add blog post: reading OSM PBF into Sedona (#3160)
02eb7b85c1d is described below

commit 02eb7b85c1d2caaeffbbe9df7fea3a376d4e8087
Author: Jia Yu <ji...@apache.org>
AuthorDate: Fri Jul 24 08:06:57 2026 -0700

    [DOCS] Add blog post: reading OSM PBF into Sedona (#3160)
---
 docs/blog/posts/osm-pbf-reader-assembly.svg |  60 ++++++++
 docs/blog/posts/osm-pbf-reader-cover.svg    |  88 ++++++++++++
 docs/blog/posts/osm-pbf-reader-scale.svg    |  74 ++++++++++
 docs/blog/posts/osm-pbf-reader.md           | 216 ++++++++++++++++++++++++++++
 4 files changed, 438 insertions(+)

diff --git a/docs/blog/posts/osm-pbf-reader-assembly.svg b/docs/blog/posts/osm-pbf-reader-assembly.svg
new file mode 100644
index 00000000000..93b049d70bf
--- /dev/null
+++ b/docs/blog/posts/osm-pbf-reader-assembly.svg
@@ -0,0 +1,60 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="1180" height="440" viewBox="0 0 1180 440" role="img" aria-label="A way stores an ordered list of node references; joining them to node coordinates and connecting them in order reconstructs a LineString">
+<defs>
+<linearGradient id="abg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#1c1022"/><stop offset="0.55" stop-color="#2a1420"/><stop offset="1" stop-color="#3a1e1b"/></linearGradient>
+</defs>
+<rect width="1180" height="440" rx="18" fill="url(#abg)"/>
+
+<text x="590" y="42" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="17" fill="#f0d3b8">a <tspan fill="#f2913a">way</tspan> is only node IDs  &#8594;  join <tspan fill="#f5c542">node coordinates</tspan>  &#8594;  connect in order = <tspan fill="#ff7a45">LineString</tspan></text>
+
+<!-- ===== CARD A: WAY ===== -->
+<g font-family="Inter, 'Helvetica Neue', Arial, sans-serif">
+<rect x="30" y="74" width="330" height="300" rx="14" fill="#2a181c" stroke="#6e3a24" stroke-width="1.5"/>
+<rect x="30" y="74" width="330" height="40" rx="14" fill="#3a2119"/><rect x="30" y="94" width="330" height="20" fill="#3a2119"/>
+<text x="50" y="100" font-family="ui-monospace, Menlo, monospace" font-size="13" font-weight="600" fill="#f2913a">WAY · id 239592573</text>
+<text x="52" y="150" font-family="ui-monospace, Menlo, monospace" font-size="13" fill="#c6ab9c">tags: {highway: <tspan fill="#f7efe7">primary</tspan>}</text>
+<text x="52" y="182" font-family="ui-monospace, Menlo, monospace" font-size="13" fill="#c6ab9c">refs:  <tspan fill="#a8897a">(ordered)</tspan></text>
+<!-- ordered ref chips -->
+<g font-family="ui-monospace, Menlo, monospace" font-size="13">
+<rect x="60" y="200" width="240" height="30" rx="8" fill="none" stroke="#f5c542" stroke-opacity="0.5" stroke-width="1.2"/>
+<text x="180" y="220" text-anchor="middle" fill="#f0d08a">101 · 102 · 103 · 104 · 105</text>
+</g>
+<text x="52" y="272" font-family="Inter, Arial, sans-serif" font-size="12.5" fill="#a8897a">posexplode(refs) keeps each</text>
+<text x="52" y="292" font-family="Inter, Arial, sans-serif" font-size="12.5" fill="#a8897a">ref with its position (0,1,2,…)</text>
+</g>
+<text x="378" y="230" text-anchor="middle" font-size="26" fill="#c6ab9c" font-family="Inter, Arial, sans-serif">&#8594;</text>
+
+<!-- ===== CARD B: NODES lookup ===== -->
+<g font-family="ui-monospace, Menlo, monospace">
+<rect x="400" y="74" width="300" height="300" rx="14" fill="#2a181c" stroke="#6e3a24" stroke-width="1.5"/>
+<rect x="400" y="74" width="300" height="40" rx="14" fill="#3a2119"/><rect x="400" y="94" width="300" height="20" fill="#3a2119"/>
+<text x="420" y="100" font-size="13" font-weight="600" fill="#f5c542">NODES · id &#8594; lon lat</text>
+<g font-size="13" fill="#e7d9cf">
+<text x="424" y="150"><tspan fill="#f5c542">101</tspan>   7.4206  43.7386</text>
+<text x="424" y="184"><tspan fill="#f5c542">102</tspan>   7.4219  43.7392</text>
+<text x="424" y="218"><tspan fill="#f5c542">103</tspan>   7.4231  43.7395</text>
+<text x="424" y="252"><tspan fill="#f5c542">104</tspan>   7.4244  43.7401</text>
+<text x="424" y="286"><tspan fill="#f5c542">105</tspan>   7.4258  43.7404</text>
+</g>
+<line x1="416" y1="162" x2="676" y2="162" stroke="#5a3226" stroke-width="1"/>
+<text x="424" y="322" font-family="Inter, Arial, sans-serif" font-size="12.5" fill="#a8897a">JOIN osm nodes ON n.id = ref</text>
+</g>
+<text x="735" y="226" text-anchor="middle" font-size="26" fill="#c6ab9c" font-family="Inter, Arial, sans-serif">&#8594;</text>
+
+<!-- ===== CARD C: LINESTRING ===== -->
+<g font-family="Inter, 'Helvetica Neue', Arial, sans-serif">
+<rect x="770" y="74" width="380" height="300" rx="14" fill="#2a181c" stroke="#ff5a2e" stroke-width="1.8"/>
+<rect x="770" y="74" width="380" height="40" rx="14" fill="#4a1e15"/><rect x="770" y="94" width="380" height="20" fill="#4a1e15"/>
+<text x="790" y="100" font-family="ui-monospace, Menlo, monospace" font-size="13" font-weight="600" fill="#ff8f5c">LINESTRING · ordered vertices</text>
+<g stroke="#8a5a3a" stroke-opacity="0.3" stroke-width="1"><line x1="770" y1="200" x2="1150" y2="200"/><line x1="770" y1="280" x2="1150" y2="280"/><line x1="880" y1="130" x2="880" y2="360"/><line x1="1000" y1="130" x2="1000" y2="360"/></g>
+<!-- the assembled line -->
+<polyline points="835,320 900,250 965,270 1035,205 1095,232" fill="none" stroke="#f2913a" stroke-width="3.4" stroke-linecap="round" stroke-linejoin="round"/>
+<!-- vertices, numbered in order -->
+<g fill="#f5c542" stroke="#2a181c" stroke-width="1.5" font-family="ui-monospace, Menlo, monospace">
+<circle cx="835" cy="320" r="8"/><circle cx="900" cy="250" r="8"/><circle cx="965" cy="270" r="8"/><circle cx="1035" cy="205" r="8"/><circle cx="1095" cy="232" r="8"/>
+</g>
+<g font-family="Inter, Arial, sans-serif" font-size="11" font-weight="700" fill="#2a181c" text-anchor="middle">
+<text x="835" y="324">1</text><text x="900" y="254">2</text><text x="965" y="274">3</text><text x="1035" y="209">4</text><text x="1095" y="236">5</text>
+</g>
+<text x="960" y="352" text-anchor="middle" font-family="ui-monospace, Menlo, monospace" font-size="12" fill="#ff9a6e">array_sort by position &#8594; connect</text>
+</g>
+</svg>
diff --git a/docs/blog/posts/osm-pbf-reader-cover.svg b/docs/blog/posts/osm-pbf-reader-cover.svg
new file mode 100644
index 00000000000..f4fa4e21a88
--- /dev/null
+++ b/docs/blog/posts/osm-pbf-reader-cover.svg
@@ -0,0 +1,88 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-label="Title band on top — OpenStreetMap, meet Sedona — above a full-width colorful city-map panorama of roads, parks, buildings, water, and points of interest">
+<defs>
+<linearGradient id="space" x1="0" y1="0" x2="0.35" y2="1"><stop offset="0" stop-color="#0b0a26"/><stop offset="0.5" stop-color="#161030"/><stop offset="1" stop-color="#241230"/></linearGradient>
+<linearGradient id="sea" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#1c4d74"/><stop offset="1" stop-color="#123a52"/></linearGradient>
+</defs>
+
+<rect width="1280" height="720" fill="url(#space)"/>
+
+<!-- ===== TOP BAND: title, no visuals ===== -->
+<text x="72" y="86" font-family="Inter, 'Helvetica Neue', Arial, sans-serif" font-size="19" font-weight="700" letter-spacing="5" fill="#f0a94e">APACHE SEDONA · OSM PBF READER</text>
+<text x="70" y="158" font-family="Inter, 'Helvetica Neue', Arial, sans-serif" font-size="58" font-weight="800" fill="#f7efe7">OpenStreetMap, meet <tspan fill="#ff7a45">Sedona</tspan>.</text>
+<text x="72" y="206" font-family="Inter, 'Helvetica Neue', Arial, sans-serif" font-size="21" font-weight="500" fill="#b6acc9">Read raw <tspan font-family="ui-monospace, Menlo, monospace" font-size="19">.osm.pbf</tspan> into a Sedona DataFrame — distributed across your whole cluster, no conversion step.</text>
+
+<rect x="72" y="238" width="452" height="48" rx="10" fill="#140f28" stroke="#3c3268" stroke-width="1.5"/>
+<text x="94" y="269" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="17" fill="#8fe6d0">sedona.read.<tspan fill="#f7efe7">format</tspan>(<tspan fill="#f5c542">"osmpbf"</tspan>).<tspan fill="#f7efe7">load</tspan>(path)</text>
+
+<g font-family="Inter, 'Helvetica Neue', Arial, sans-serif" font-size="15">
+<rect x="548" y="245" width="132" height="34" rx="17" fill="none" stroke="#f5c542" stroke-opacity="0.6" stroke-width="1.3"/><text x="614" y="267" text-anchor="middle" fill="#f0d08a">nodes &#8594; points</text>
+<rect x="694" y="245" width="126" height="34" rx="17" fill="none" stroke="#35c9d8" stroke-opacity="0.6" stroke-width="1.3"/><text x="757" y="267" text-anchor="middle" fill="#5fd8e0">ways &#8594; lines</text>
+<rect x="834" y="245" width="128" height="34" rx="17" fill="none" stroke="#8a6ff0" stroke-opacity="0.6" stroke-width="1.3"/><text x="898" y="267" text-anchor="middle" fill="#ab97f5">tags &#8594; layers</text>
+<rect x="976" y="245" width="160" height="34" rx="17" fill="none" stroke="#7ed957" stroke-opacity="0.6" stroke-width="1.3"/><text x="1056" y="267" text-anchor="middle" fill="#98e07a">1 file &#8594; N workers</text>
+</g>
+
+<!-- ===== BOTTOM: full-width map panorama (no text overlay) ===== -->
+<!-- faint graticule, map area only -->
+<g stroke="#7d84c8" stroke-opacity="0.08" stroke-width="1">
+<line x1="160" y1="320" x2="160" y2="684"/><line x1="320" y1="320" x2="320" y2="684"/><line x1="480" y1="320" x2="480" y2="684"/><line x1="640" y1="320" x2="640" y2="684"/><line x1="800" y1="320" x2="800" y2="684"/><line x1="960" y1="320" x2="960" y2="684"/><line x1="1120" y1="320" x2="1120" y2="684"/>
+<line x1="0" y1="380" x2="1280" y2="380"/><line x1="0" y1="470" x2="1280" y2="470"/><line x1="0" y1="560" x2="1280" y2="560"/><line x1="0" y1="650" x2="1280" y2="650"/></g>
+
+<!-- water -->
+<path d="M1000,684 C1024,590 1120,548 1280,562 L1280,684 Z" fill="url(#sea)"/>
+<path d="M1064,620 q46,-20 96,-6 M1110,662 q40,-16 86,-4" stroke="#5fd8e0" stroke-opacity="0.35" stroke-width="1.6" fill="none"/>
+<path d="M70,410 q66,-30 126,2 q40,26 8,56 q-48,40 -112,16 q-58,-22 -22,-74 Z" fill="url(#sea)" opacity="0.85"/>
+
+<!-- parks -->
+<path d="M950,380 q64,-28 124,0 q40,22 12,56 q-38,42 -102,26 q-68,-22 -34,-82 Z" fill="#2fd2a8" fill-opacity="0.26" stroke="#2fd2a8" stroke-width="1.5"/>
+<path d="M170,570 q60,-34 126,-8 q44,20 18,60 q-32,48 -102,34 q-80,-20 -42,-86 Z" fill="#7ed957" fill-opacity="0.24" stroke="#7ed957" stroke-width="1.5"/>
+<path d="M580,610 q52,-24 100,0 q28,18 8,44 q-30,32 -82,18 q-54,-14 -26,-62 Z" fill="#2fd2a8" fill-opacity="0.2" stroke="#2fd2a8" stroke-width="1.3"/>
+
+<!-- buildings -->
+<g fill="#8a6ff0" fill-opacity="0.5" stroke="#8a6ff0" stroke-width="1">
+<rect x="330" y="370" width="34" height="26" transform="rotate(8 347 383)"/>
+<rect x="374" y="362" width="26" height="34" transform="rotate(8 387 379)"/>
+<rect x="342" y="416" width="40" height="24" transform="rotate(8 362 428)"/>
+<rect x="1075" y="440" width="30" height="26" transform="rotate(14 1090 453)"/>
+<rect x="1113" y="430" width="24" height="32" transform="rotate(14 1125 446)"/>
+<rect x="430" y="590" width="30" height="24" transform="rotate(-6 445 602)"/>
+<rect x="468" y="618" width="38" height="22" transform="rotate(-6 487 629)"/>
+<rect x="835" y="600" width="30" height="24" transform="rotate(5 850 612)"/>
+</g>
+
+<!-- roads -->
+<g fill="none" stroke-linecap="round" stroke-linejoin="round">
+<path d="M-20,600 L240,545 L520,505 L800,465 L1080,415 L1300,385" stroke="#f5c542" stroke-width="5"/>
+<path d="M240,545 L340,430 L500,375 L680,345" stroke="#35c9d8" stroke-width="3.4"/>
+<path d="M800,465 L920,545 L1080,610 L1240,655" stroke="#35c9d8" stroke-width="3.4"/>
+<g stroke="#4d8ff5" stroke-width="2.2">
+<path d="M340,430 L500,455 L520,505"/>
+<path d="M500,375 L640,395 L780,375"/>
+<path d="M520,505 L600,590 L580,612"/>
+<path d="M920,545 L990,480 L1080,415"/>
+</g>
+<g stroke="#ff6a9a" stroke-width="1.8" stroke-dasharray="5 5">
+<path d="M150,470 Q250,450 340,430"/>
+<path d="M1080,415 Q1030,380 980,352"/>
+<path d="M600,590 Q720,600 835,612"/>
+</g>
+</g>
+
+<!-- junction nodes -->
+<g fill="#e9ecff" stroke="#0b0a26" stroke-width="1.4">
+<circle cx="240" cy="545" r="5"/><circle cx="520" cy="505" r="5"/><circle cx="800" cy="465" r="5"/><circle cx="1080" cy="415" r="5"/><circle cx="340" cy="430" r="5"/><circle cx="500" cy="375" r="5"/><circle cx="920" cy="545" r="5"/><circle cx="600" cy="590" r="5"/><circle cx="500" cy="455" r="5"/></g>
+
+<!-- POI pins -->
+<g>
+<g transform="translate(225,505)"><path d="M0,0 C-10,-15 -10,-28 0,-28 C10,-28 10,-15 0,0 Z" fill="#ff7a45"/><circle cx="0" cy="-19" r="3.6" fill="#0b0a26"/></g>
+<g transform="translate(905,400)"><path d="M0,0 C-10,-15 -10,-28 0,-28 C10,-28 10,-15 0,0 Z" fill="#ff7a45"/><circle cx="0" cy="-19" r="3.6" fill="#0b0a26"/></g>
+<g transform="translate(1150,520)"><path d="M0,0 C-10,-15 -10,-28 0,-28 C10,-28 10,-15 0,0 Z" fill="#ff7a45"/><circle cx="0" cy="-19" r="3.6" fill="#0b0a26"/></g>
+<g transform="translate(690,560)"><path d="M0,0 C-10,-15 -10,-28 0,-28 C10,-28 10,-15 0,0 Z" fill="#ff7a45"/><circle cx="0" cy="-19" r="3.6" fill="#0b0a26"/></g>
+</g>
+
+<!-- boundary relation frames the map strip -->
+<polygon points="34,332 1246,326 1252,678 28,674" fill="none" stroke="#f5c542" stroke-opacity="0.4" stroke-width="1.6" stroke-dasharray="8 6"/>
+<text x="50" y="352" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="12.5" fill="#a9b3e0">relation · boundary</text>
+
+<!-- legend below the map -->
+<text x="640" y="708" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="13" fill="#a9b3e0"><tspan fill="#e9ecff">●</tspan> nodes  <tspan fill="#f5c542">—</tspan> ways  <tspan fill="#ff7a45">◍</tspan> amenity  <tspan fill="#8a6ff0">▮</tspan> building  <tspan fill="#7ed957">▲</tspan> park  <tspan fill="#5fd8e0">≈</tspan> water</text>
+</svg>
diff --git a/docs/blog/posts/osm-pbf-reader-scale.svg b/docs/blog/posts/osm-pbf-reader-scale.svg
new file mode 100644
index 00000000000..3f140594439
--- /dev/null
+++ b/docs/blog/posts/osm-pbf-reader-scale.svg
@@ -0,0 +1,74 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="1180" height="380" viewBox="0 0 1180 380" role="img" aria-label="A single planet-scale .osm.pbf file is split into byte-range blocks, decoded in parallel by many workers, and lands as one Sedona DataFrame">
+<defs>
+<linearGradient id="sbg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#1c1022"/><stop offset="0.55" stop-color="#2a1420"/><stop offset="1" stop-color="#3a1e1b"/></linearGradient>
+</defs>
+<rect width="1180" height="380" rx="18" fill="url(#sbg)"/>
+
+<text x="590" y="40" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="17" fill="#f0d3b8">one <tspan fill="#f5c542">~80 GB planet file</tspan> &#8594; byte-range splits &#8594; <tspan fill="#f2913a">parallel decode</tspan> &#8594; one <tspan fill="#ff7a45">Sedona DataFrame</tspan></text>
+
+<!-- ===== the single file, segmented into independently decodable blocks ===== -->
+<text x="60" y="128" font-family="ui-monospace, Menlo, monospace" font-size="13" fill="#c6ab9c">s3://osm/planet-latest.osm.pbf</text>
+<rect x="60" y="142" width="270" height="96" rx="10" fill="#2a181c" stroke="#f5c542" stroke-width="1.6"/>
+<!-- block separators -->
+<g stroke="#f5c542" stroke-opacity="0.55" stroke-width="1.2" stroke-dasharray="4 4">
+<line x1="127" y1="142" x2="127" y2="238"/><line x1="194" y1="142" x2="194" y2="238"/><line x1="261" y1="142" x2="261" y2="238"/>
+</g>
+<g font-family="ui-monospace, Menlo, monospace" font-size="11.5" fill="#f0d08a" text-anchor="middle">
+<text x="93" y="195">blk 0</text><text x="160" y="195">blk 1</text><text x="227" y="195">blk 2</text><text x="294" y="195">&#8230;</text>
+</g>
+<text x="60" y="266" font-family="Inter, 'Helvetica Neue', Arial, sans-serif" font-size="12.5" fill="#a8897a">PBF blocks decode independently —</text>
+<text x="60" y="284" font-family="Inter, 'Helvetica Neue', Arial, sans-serif" font-size="12.5" fill="#a8897a">the reader is splittable, no pre-chopping</text>
+
+<!-- fan-out arrows to workers -->
+<g stroke="#f2913a" stroke-width="1.8" fill="none">
+<path d="M334,168 C400,120 440,104 486,96"/>
+<path d="M334,190 C400,178 440,172 486,168"/>
+<path d="M334,212 C400,224 440,234 486,240"/>
+<path d="M334,232 C400,278 440,300 486,310"/>
+</g>
+<g fill="#f2913a"><path d="M486,96 l-10,-4 2,8 Z"/><path d="M486,168 l-10,-4 2,8 Z"/><path d="M486,240 l-10,-4 2,8 Z"/><path d="M486,310 l-10,-4 2,8 Z"/></g>
+
+<!-- ===== workers, decoding in parallel ===== -->
+<g font-family="ui-monospace, Menlo, monospace" font-size="12.5">
+<g>
+<rect x="496" y="72" width="230" height="50" rx="10" fill="#2a181c" stroke="#6e3a24" stroke-width="1.5"/>
+<circle cx="520" cy="97" r="6" fill="#f2913a"/><text x="536" y="102" fill="#e7d9cf">worker 1 &#183; blk 0</text>
+<rect x="664" y="90" width="50" height="14" rx="7" fill="#3a231d"/><rect x="664" y="90" width="40" height="14" rx="7" fill="#f2913a"/>
+</g>
+<g>
+<rect x="496" y="144" width="230" height="50" rx="10" fill="#2a181c" stroke="#6e3a24" stroke-width="1.5"/>
+<circle cx="520" cy="169" r="6" fill="#f5c542"/><text x="536" y="174" fill="#e7d9cf">worker 2 &#183; blk 1</text>
+<rect x="664" y="162" width="50" height="14" rx="7" fill="#3a231d"/><rect x="664" y="162" width="32" height="14" rx="7" fill="#f5c542"/>
+</g>
+<g>
+<rect x="496" y="216" width="230" height="50" rx="10" fill="#2a181c" stroke="#6e3a24" stroke-width="1.5"/>
+<circle cx="520" cy="241" r="6" fill="#ff7a45"/><text x="536" y="246" fill="#e7d9cf">worker 3 &#183; blk 2</text>
+<rect x="664" y="234" width="50" height="14" rx="7" fill="#3a231d"/><rect x="664" y="234" width="45" height="14" rx="7" fill="#ff7a45"/>
+</g>
+<g>
+<rect x="496" y="288" width="230" height="50" rx="10" fill="#2a181c" stroke="#6e3a24" stroke-width="1.5" stroke-dasharray="5 4"/>
+<text x="536" y="318" fill="#a8897a">worker N &#183; blk &#8230;</text>
+</g>
+</g>
+<text x="611" y="62" text-anchor="middle" font-family="Inter, 'Helvetica Neue', Arial, sans-serif" font-size="12.5" fill="#a8897a">decoding in parallel</text>
+
+<!-- converge arrows -->
+<g stroke="#ff7a45" stroke-width="1.8" fill="none">
+<path d="M726,97 C790,110 820,140 856,166"/>
+<path d="M726,169 C790,175 820,180 856,186"/>
+<path d="M726,241 C790,232 820,220 856,206"/>
+<path d="M726,313 C790,290 820,254 856,226"/>
+</g>
+<g fill="#ff7a45"><path d="M856,166 l-10,-6 0,9 Z"/><path d="M856,186 l-10,-5 1,9 Z"/><path d="M856,206 l-10,-2 2,9 Z"/><path d="M856,226 l-10,0 4,9 Z"/></g>
+
+<!-- ===== one DataFrame ===== -->
+<rect x="866" y="120" width="254" height="170" rx="12" fill="#2a181c" stroke="#ff5a2e" stroke-width="1.8"/>
+<rect x="866" y="120" width="254" height="38" rx="12" fill="#4a1e15"/><rect x="866" y="140" width="254" height="18" fill="#4a1e15"/>
+<text x="884" y="145" font-family="ui-monospace, Menlo, monospace" font-size="13" font-weight="600" fill="#ff8f5c">one Sedona DataFrame</text>
+<g font-family="ui-monospace, Menlo, monospace" font-size="12" fill="#e7d9cf">
+<text x="884" y="184"><tspan fill="#f5c542">node</tspan>      9 billion+</text>
+<text x="884" y="212"><tspan fill="#f2913a">way</tspan>       1 billion+</text>
+<text x="884" y="240"><tspan fill="#ff7a45">relation</tspan>  millions</text>
+</g>
+<text x="884" y="272" font-family="Inter, 'Helvetica Neue', Arial, sans-serif" font-size="12" fill="#a8897a">same queries, more partitions</text>
+</svg>
diff --git a/docs/blog/posts/osm-pbf-reader.md b/docs/blog/posts/osm-pbf-reader.md
new file mode 100644
index 00000000000..a3cf6adc538
--- /dev/null
+++ b/docs/blog/posts/osm-pbf-reader.md
@@ -0,0 +1,216 @@
+---
+date:
+  created: 2026-07-24
+links:
+  - Geofabrik downloads (regional .osm.pbf): https://download.geofabrik.de/
+  - OpenStreetMap: https://www.openstreetmap.org/
+  - Spatial DataFrame / SQL app: https://sedona.apache.org/latest/tutorial/sql/
+authors:
+  - jia
+title: "OpenStreetMap, Meet Sedona: Raw .osm.pbf to Spatial SQL"
+---
+
+# OpenStreetMap, Meet Sedona: Raw .osm.pbf to Spatial SQL
+
+OpenStreetMap is the world's map — every road, café, and coastline, edited by millions of people. It ships as `.osm.pbf`: a dense, compressed Protocol-Buffers blob of nodes, ways, and relations. Getting that into a cluster usually means a preprocessing detour through `osmium` or a staging database.
+
+![Apache Sedona reads a raw .osm.pbf file straight into a Sedona DataFrame, turning OpenStreetMap nodes, ways, and relations into points and lines](osm-pbf-reader-cover.svg)
+
+<!-- more -->
+
+SedonaSpark reads `.osm.pbf` natively. Point the `osmpbf` format at a file and you get a Sedona DataFrame of raw OSM elements — no conversion step. And because Sedona is a distributed engine, that one-line read fans out across a cluster: a planet-scale file is just more partitions. From there it's ordinary Spatial SQL: assemble geometries, filter by tag, measure, join. Every snippet below runs against the small **Monaco** extract that ships in Sedona's test resources, so you can reproduc [...]
+
+## One line to raw OSM
+
+```python
+from sedona.spark import SedonaContext
+
+sedona = SedonaContext.create(SedonaContext.builder().master("local[*]").getOrCreate())
+
+# a regional extract from Geofabrik, or Sedona's bundled Monaco sample
+osm = sedona.read.format("osmpbf").load(
+    "spark/common/src/test/resources/osmpbf/monaco-latest.osm.pbf"
+)
+osm.createOrReplaceTempView("osm")
+osm.printSchema()
+```
+
+The reader gives you the raw OSM element model, one row per element:
+
+```
+ |-- id: long
+ |-- kind: string                 (node | way | relation)
+ |-- location: struct<longitude, latitude>   -- nodes only
+ |-- tags: map<string, string>
+ |-- refs: array<long>            -- ordered member node/way ids (ways & relations)
+ |-- ref_roles: array<string>
+ |-- ref_types: array<string>
+ |-- changeset / timestamp / uid / user / version / visible   -- edit metadata
+```
+
+Monaco is small but complete — three element kinds, one table:
+
+```python
+sedona.sql("SELECT kind, COUNT(*) AS n FROM osm GROUP BY kind ORDER BY n DESC").show()
+```
+
+```
++--------+-----+
+|kind    |n    |
++--------+-----+
+|node    |39587|
+|way     | 5777|
+|relation|  309|
++--------+-----+
+```
+
+## Nodes are points
+
+A **node** is the only element with a coordinate, so points are immediate. Here are Monaco's amenities:
+
+```python
+sedona.sql("""
+    SELECT id, tags['amenity'] AS amenity, tags['name'] AS name,
+           ST_ReducePrecision(ST_Point(location.longitude, location.latitude), 5) AS geom
+    FROM   osm
+    WHERE  kind = 'node' AND tags['amenity'] IS NOT NULL
+    ORDER BY id LIMIT 5
+""").show(truncate=False)
+```
+
+```
++--------+-------+------------------------------+------------------------+
+|id      |amenity|name                          |geom                    |
++--------+-------+------------------------------+------------------------+
+|25191432|parking|Parking du Chemin des Pêcheurs|POINT (7.42711 43.73128)|
+|25230434|fuel   |Esso                          |POINT (7.41209 43.72851)|
+|25239190|parking|null                          |POINT (7.42919 43.74531)|
+|25239191|parking|Parking du centre commercial  |POINT (7.41792 43.7309) |
+|25249199|parking|Parking de la Gare            |POINT (7.41908 43.7389) |
++--------+-------+------------------------------+------------------------+
+```
+
+## Ways are just node IDs — assemble the lines
+
+Here's the part that trips people up. A **way** (a road, a river, a building outline) has **no coordinates of its own** — only `refs`, an *ordered* list of the node IDs it passes through. To get a geometry you resolve those IDs back to node coordinates, in order, and connect them.
+
+![An OSM way stores an ordered list of node references; joining refs back to node coordinates and connecting them in sequence reconstructs the LineString](osm-pbf-reader-assembly.svg)
+
+That's a `posexplode` to keep the ordering, a join to the nodes, and a rebuild in sequence:
+
+```python
+roads = sedona.sql("""
+    WITH nodes AS (
+        SELECT id, location.longitude AS lon, location.latitude AS lat
+        FROM osm WHERE kind = 'node'
+    ),
+    way_pts AS (                               -- one row per (way, vertex), ordered
+        SELECT w.id AS way_id, w.highway, w.pos AS seq, n.lon, n.lat
+        FROM (SELECT id, tags['highway'] AS highway, posexplode(refs) AS (pos, ref)
+              FROM osm WHERE kind = 'way' AND tags['highway'] IS NOT NULL) w
+        JOIN nodes n ON n.id = w.ref
+    )
+    SELECT way_id, highway,
+           ST_MakeLine(
+               transform(array_sort(collect_list(struct(seq, lon, lat))),
+                         x -> ST_Point(x.lon, x.lat))
+           ) AS geom
+    FROM   way_pts
+    GROUP BY way_id, highway
+    HAVING count(*) >= 2
+""")
+roads.createOrReplaceTempView("roads")
+```
+
+`array_sort` on `(seq, …)` guarantees the vertices land in the way's declared order, and `ST_MakeLine` connects the sorted points into the geometry — no string assembly. Note the tag filter sits *before* the join, so only road vertices are ever shuffled. Now the ways are real `LineString`s and the full Spatial SQL toolkit applies. How much road is in Monaco?
+
+```python
+sedona.sql("""
+    SELECT highway,
+           COUNT(*) AS ways,
+           ROUND(SUM(ST_LengthSpheroid(geom)) / 1000, 1) AS km
+    FROM   roads
+    GROUP BY highway
+    ORDER BY km DESC
+    LIMIT 5
+""").show(truncate=False)
+```
+
+```
++-----------+----+----+
+|highway    |ways|km  |
++-----------+----+----+
+|footway    |1612|63.5|
+|residential| 311|29.0|
+|service    | 379|19.6|
+|tertiary   | 127|10.6|
+|secondary  | 174|10.5|
++-----------+----+----+
+```
+
+**3,334 road ways, 163.9 km** of it — reconstructed from raw node references, no preprocessing. (The same pattern builds rivers, boundaries, or coastlines; you just filter a different tag.)
+
+## Every element carries its history
+
+OSM elements are versioned. Each row keeps a `version` and an edit `timestamp`, so you can measure how *alive* a map is — no separate history file. When was Monaco mapped?
+
+```python
+sedona.sql("""
+    SELECT YEAR(timestamp) AS yr, COUNT(*) AS edits
+    FROM   osm WHERE timestamp IS NOT NULL
+    GROUP BY YEAR(timestamp) ORDER BY edits DESC LIMIT 5
+""").show()
+```
+
+```
++----+-----+
+|  yr|edits|
++----+-----+
+|2023| 8595|
+|2011| 5206|
+|2022| 5175|
+|2024| 4110|
+|2020| 3865|
++----+-----+
+```
+
+And which features have been revised the most? `version` tells you — the big administrative-boundary relations churn constantly:
+
+```python
+sedona.sql("""
+    SELECT kind, tags['name'] AS name, version
+    FROM   osm WHERE tags['name'] IS NOT NULL
+    ORDER BY version DESC LIMIT 3
+""").show(truncate=False)
+```
+
+```
++--------+--------------------------+-------+
+|kind    |name                      |version|
++--------+--------------------------+-------+
+|relation|France métropolitaine     |800    |
+|relation|Provence-Alpes-Côte d'Azur|774    |
+|relation|France (terres)           |614    |
++--------+--------------------------+-------+
+```
+
+!!! note "A note on the metadata fields"
+    The reader also exposes `user`, `uid`, and `changeset`. Public Geofabrik extracts **strip these for privacy**, so they arrive empty — as they do here. To analyze contributors by name you need a full-history or internal planet file; `timestamp` and `version` are always present.
+
+## Built for the planet, not the demo
+
+Monaco is a teaching extract — the full OpenStreetMap planet is a *single* `.osm.pbf` of roughly 80 GB holding billions of elements. Three properties of the reader make that size a non-event on a Sedona cluster:
+
+![A single planet-scale .osm.pbf is split into byte-range blocks, decoded in parallel by many workers, and lands as one Sedona DataFrame](osm-pbf-reader-scale.svg)
+
+- **One file, many workers.** PBF is a sequence of independently decodable blocks, and the reader declares the format *splittable* — Sedona carves even a single planet file into byte-range partitions and decodes them in parallel across the cluster. No pre-chopping into per-region files.
+- **Object storage native.** The same `load()` reads from S3 or HDFS as happily as from a local disk, so the planet file never has to leave your bucket.
+- **Assembly is a join, not a script.** The refs → coordinates rebuild above is an ordinary distributed join between two slices of the same table. Where a single-machine tool streams the planet through one process, Sedona shuffles the work across however many machines you give it — the classic way OSM processing becomes horizontal instead of overnight.
+
+The result: the four queries in this post run against `planet-latest.osm.pbf` exactly as written — just with more partitions underneath.
+
+## The point
+
+`.osm.pbf` stops being a format problem. One `osmpbf` read turns the planet's map into a Sedona DataFrame; `posexplode` + a join reconstitutes geometries from raw references; and from there OpenStreetMap is just another spatial table — one you can filter, measure, and join at whatever scale your cluster allows.
+
+*Grab any regional extract from [Geofabrik](https://download.geofabrik.de/) — or the planet itself — and point the reader at it: the queries above scale from Monaco to the whole world unchanged.*