<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ahsan Mehmood</title>
    <description>The latest articles on DEV Community by Ahsan Mehmood (@iamahsanmehmood).</description>
    <link>https://dev.to/iamahsanmehmood</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3813758%2F4e51aeb9-4998-4138-a021-26bb36662d98.jpg</url>
      <title>DEV Community: Ahsan Mehmood</title>
      <link>https://dev.to/iamahsanmehmood</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/iamahsanmehmood"/>
    <language>en</language>
    <item>
      <title>We Let AI Build SketchUp Models With Nothing But an API</title>
      <dc:creator>Ahsan Mehmood</dc:creator>
      <pubDate>Thu, 20 Aug 2026 13:44:08 +0000</pubDate>
      <link>https://dev.to/iamahsanmehmood/we-let-ai-build-sketchup-models-with-nothing-but-an-api-3d0a</link>
      <guid>https://dev.to/iamahsanmehmood/we-let-ai-build-sketchup-models-with-nothing-but-an-api-3d0a</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/iamahsanmehmood/openskp" rel="noopener noreferrer"&gt;OpenSKP&lt;/a&gt;'s writer API has no &lt;code&gt;Chair&lt;/code&gt; class. No &lt;code&gt;Table&lt;/code&gt; builder, no &lt;code&gt;furniture&lt;/code&gt; module, nothing that knows what a chair is. It exposes materials, layers, groups, component definitions, and faces — the same primitives SketchUp itself is built from — and nothing above that. Which raised an obvious question once the writer shipped: could an AI coding agent, given only that low-level API and a plain-English description of an object, produce a real, correctly-structured &lt;code&gt;.skp&lt;/code&gt; file?&lt;/p&gt;

&lt;p&gt;We ran the experiment with two different AI coding agents, independently, with no shared prompt engineering between the runs. Both were pointed at the same generic API and asked to build furniture. Neither was given examples of chair geometry or table dimensions to crib from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chair, table, and an armchair
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcg6x8feyj9s1q9ggx6tq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcg6x8feyj9s1q9ggx6tq.png" alt="AI-generated chair, side table, and armchair rendered in the OpenSKP web viewer" width="800" height="654"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Output from an AI agent's own generated Python script, loaded straight into the OpenSKP web viewer with no manual cleanup.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The first run produced a scene with a dining chair, a side table, and an armchair — nine component definitions, thirty-eight geometry primitives in total, organized across three layers with four distinct materials. That structure wasn't hand-specified; the agent decided the layer and material breakdown itself, deriving it from how it reasoned about the objects (seat vs. legs vs. backrest as separate faces sharing a wood material, for instance).&lt;/p&gt;

&lt;p&gt;Here's a representative excerpt of what the agent actually wrote, generating a chair leg as an extruded rectangular profile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_leg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;material&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Add a single tapered leg as a box primitive.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;pts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pts&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;face_bottom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_face&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;material&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;material&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;face_top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_face&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;material&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;material&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;side&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;pts&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
        &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_face&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;side&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;material&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;material&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;face_bottom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;face_top&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing in that function is chair-specific — it's a general box-extrusion routine, the kind of primitive-geometry reasoning you'd expect from someone who understands 3D coordinate systems, not someone who was handed a chair template. The agent built its own mental model of "chair" out of boxes and faces, the same way a human modeler would work from scratch in SketchUp's own polygon tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  An executive desk
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9nc1cjj5q6e9ok7g6ce2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9nc1cjj5q6e9ok7g6ce2.png" alt="AI-generated executive desk with drawers rendered in the OpenSKP web viewer" width="782" height="671"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A more complex single object: a desk with a drawer unit, modeled as nested component groups.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The second test pushed further into nested structure: a desk with an attached drawer unit, modeled as a component group nested inside the desk's top-level group — mirroring how a careful human SketchUp modeler would organize the same object, with the drawer as its own reusable component rather than geometry welded directly into the desktop.&lt;/p&gt;

&lt;h2&gt;
  
  
  A phone, viewed from both sides
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkamcwhbsy1m9w5x4dipc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkamcwhbsy1m9w5x4dipc.png" alt="AI-generated phone model shown from front and back in the OpenSKP web viewer" width="800" height="336"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Front and back views of the same AI-generated phone model — face winding and normals came out correct on the first attempt.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The third object, from the second independent agent run, was a simplified phone: a thin rounded body, a screen face, and a camera module. What's notable here isn't the object's complexity — it's the simplest of the three — but that face winding order came out correct without being told about it explicitly. Get winding backward and a face renders as invisible or inside-out from the "wrong" side; the model shown above renders correctly from both front and back, which means the agent's geometry reasoning implicitly respected consistent counter-clockwise winding, not just "some points that happen to form a face."&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does and doesn't prove
&lt;/h2&gt;

&lt;p&gt;This isn't a claim that AI agents can replace a SketchUp modeler for genuinely complex or organic geometry — everything shown here is bounded, rectilinear furniture-and-electronics geometry, well within what an LLM can reason about symbolically in coordinates and box-extrusions. What it does show is that OpenSKP's writer API is low-level enough, and clean enough, that an agent with no domain-specific tooling can drive it correctly: valid component hierarchies, sane material and layer organization, and geometry SketchUp itself opens without complaint.&lt;/p&gt;

&lt;p&gt;That's the actual target for the API design — not "convenient for humans typing by hand," but "reasonable for a coding agent to drive from a plain description," since increasingly, that's who's calling it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Try it yourself: &lt;a href="https://github.com/iamahsanmehmood/openskp" rel="noopener noreferrer"&gt;github.com/iamahsanmehmood/openskp&lt;/a&gt; — MIT licensed, available for Python, TypeScript, .NET, Dart, and C++.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>skp</category>
      <category>sketchup</category>
    </item>
    <item>
      <title>OpenSKP 1.1.0: A Native .skp Writer, Now in All Five Languages</title>
      <dc:creator>Ahsan Mehmood</dc:creator>
      <pubDate>Thu, 20 Aug 2026 13:35:52 +0000</pubDate>
      <link>https://dev.to/iamahsanmehmood/openskp-110-a-native-skp-writer-now-in-all-five-languages-5d0m</link>
      <guid>https://dev.to/iamahsanmehmood/openskp-110-a-native-skp-writer-now-in-all-five-languages-5d0m</guid>
      <description>&lt;p&gt;Until 1.1.0, &lt;a href="https://github.com/iamahsanmehmood/openskp" rel="noopener noreferrer"&gt;OpenSKP&lt;/a&gt; was a read path. You could parse a &lt;code&gt;.skp&lt;/code&gt; file — either container format, all five languages — and get materials, layers, geometry, and metadata out of it. What you couldn't do was make a new one. 1.1.0 closes that gap: a real writer, producing files SketchUp itself opens without complaint, shipped in Python, TypeScript, .NET, Dart, and C++ at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why write support is a different problem than read support
&lt;/h2&gt;

&lt;p&gt;Parsing forgives sloppiness in a way writing doesn't. A parser that mishandles an obscure edge case just gets slightly wrong data for that one field — annoying, but survivable, and often invisible unless you're specifically checking. A writer that gets the container format wrong produces a file that SketchUp refuses to open at all, or worse, opens with silently corrupted geometry. There's no partial credit.&lt;/p&gt;

&lt;p&gt;That asymmetry shaped how the writer got built. It targets the modern &lt;strong&gt;VFF&lt;/strong&gt; container exclusively — the ZIP-based, TLV-tree format used by SketchUp 2021 and later — rather than also targeting the legacy MFC stream format, because VFF is what every current SketchUp install expects by default and where new files should live going forward. Every writer feature was validated the same way the parser's correctness was validated originally: produce a file, open it with the real SketchUp SDK, and confirm the SDK's own reading of geometry, materials, and layer assignments matches what was intended, field by field — not just "the file has a size greater than zero and doesn't crash on open."&lt;/p&gt;

&lt;h2&gt;
  
  
  What the writer actually does
&lt;/h2&gt;

&lt;p&gt;The API surface is intentionally generic rather than shaped around specific object types. There's no &lt;code&gt;Chair&lt;/code&gt; class or &lt;code&gt;Table&lt;/code&gt; builder baked into the library — you compose scenes out of the same primitives SketchUp itself works with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Materials&lt;/strong&gt; — solid colors, opacity, and image textures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layers (tags)&lt;/strong&gt; — for organizing geometry the way SketchUp's own Tags panel does&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Groups and component definitions&lt;/strong&gt; — the same grouping/instancing model SketchUp uses natively, including nested groups&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Curves and faces&lt;/strong&gt; — arbitrary polygon geometry with material and layer assignment per face&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;create()&lt;/code&gt; for a brand-new file, and &lt;code&gt;open_existing()&lt;/code&gt; for loading a file, editing it, and writing it back out&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;open_existing()&lt;/code&gt; in particular was the harder of the two entry points to get right. It isn't a byte-patcher that finds the one chunk you touched and surgically rewrites it in place — that approach is fragile against any structural change (add a face, and every downstream offset in the container shifts). Instead it does a full parse of the existing file into the same in-memory model the writer already knows how to serialize, applies your edits to that model, and replays the entire thing back out through the same write path a brand-new file goes through. Slower than a patch, but correct by construction: if &lt;code&gt;create()&lt;/code&gt; is trustworthy, &lt;code&gt;open_existing()&lt;/code&gt; inherits that trust instead of needing its own separate proof.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five ports, five different sets of bugs
&lt;/h2&gt;

&lt;p&gt;The writer shipped in Python first, since Python was already the most mature of the five ports and the fastest place to validate the design against the real SDK. Porting it to the other four languages wasn't a mechanical translation exercise — each port's own CI caught real, language-specific problems that the Python reference implementation simply couldn't have surfaced:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;C++&lt;/strong&gt; — a &lt;code&gt;check_writable&lt;/code&gt; helper failed to compile under the CI's stricter build flags, a class of error Python's dynamic typing has no equivalent to catching until runtime, if at all. The C++ port also turned up a &lt;code&gt;clang-format&lt;/code&gt; issue where the CI's diff report was silently truncating on longer violations, which needed fixing in the tooling itself before it could be trusted to gate anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dart&lt;/strong&gt; — trigonometric rounding in transform-matrix math didn't match the other languages' output bit-for-bit, traced to a difference in how Dart's math library rounds versus Python's, and fixed by aligning the rounding step explicitly rather than relying on each language's default behavior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two of the ports&lt;/strong&gt; hit test-ordering bugs — tests that passed individually but failed when run as part of the full suite, because they shared mutable fixture state that Python's test runner happened to isolate in a way the other runner didn't by default.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these were writer-logic bugs in the sense of "the geometry is wrong." They were the ordinary friction of porting real, non-trivial code across five different type systems, build toolchains, and test runners — exactly the kind of thing that's invisible if you only ever ship one language, and exactly why keeping five active ports is more expensive than it sounds, but catches more than a single-language project ever would.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shipping five packages at once
&lt;/h2&gt;

&lt;p&gt;1.1.0 went out to all five registries in the same release cycle — PyPI, npm, NuGet, pub.dev, and a tagged GitHub Release with prebuilt C++ artifacts (C++ has no package registry equivalent, so it ships as a downloadable tarball/zip pair instead). Coordinating that many release pipelines in one pass surfaced two more process-level snags worth naming honestly: a batched multi-language tag push needed the tags separated out per-language rather than pushed as one lump, and pub.dev's own publish flow has a gotcha around tag naming that isn't obvious until it rejects a push.&lt;/p&gt;

&lt;p&gt;Neither was a code bug. Both are the kind of thing you only learn by actually running a five-language release end to end, which is exactly what 1.1.0 forced.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Read and write both exist now, but write support is currently VFF-only — no legacy-MFC writing, and no re-encoding a legacy file into the modern container. Conversion the other direction (glTF, IFC, OBJ into &lt;code&gt;.skp&lt;/code&gt;) is on the roadmap but not started. If either of those is something you'd use, the &lt;a href="https://github.com/iamahsanmehmood/openskp/issues" rel="noopener noreferrer"&gt;issue tracker&lt;/a&gt; is open.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Install:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;openskp          &lt;span class="c"&gt;# Python&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;openskp          &lt;span class="c"&gt;# TypeScript / JavaScript&lt;/span&gt;
dotnet add package OpenSKP   &lt;span class="c"&gt;# .NET&lt;/span&gt;
dart pub add openskp         &lt;span class="c"&gt;# Dart&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;OpenSKP is open source under the MIT license: &lt;a href="https://github.com/iamahsanmehmood/openskp" rel="noopener noreferrer"&gt;github.com/iamahsanmehmood/openskp&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>opensource</category>
      <category>python</category>
    </item>
    <item>
      <title>Why We Built OpenSKP</title>
      <dc:creator>Ahsan Mehmood</dc:creator>
      <pubDate>Thu, 20 Aug 2026 13:22:37 +0000</pubDate>
      <link>https://dev.to/iamahsanmehmood/why-we-built-openskp-4g8d</link>
      <guid>https://dev.to/iamahsanmehmood/why-we-built-openskp-4g8d</guid>
      <description>&lt;h2&gt;
  
  
  Why We Built OpenSKP
&lt;/h2&gt;

&lt;p&gt;If you want to read a &lt;code&gt;.dxf&lt;/code&gt; file, there's an open specification. If you want to read a &lt;code&gt;.gltf&lt;/code&gt; file, there's a Khronos Group standard with a public GitHub repo and a validator you can run against your output. If you want to read a &lt;code&gt;.skp&lt;/code&gt; file — SketchUp's own native format — there is nothing. No spec, no schema, no reference decoder. Just a proprietary SDK, licensed by Trimble, that only runs where Trimble lets it run.&lt;/p&gt;

&lt;p&gt;That gap is the entire reason &lt;a href="https://github.com/iamahsanmehmood/openskp" rel="noopener noreferrer"&gt;OpenSKP&lt;/a&gt; exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with "just use the SDK"
&lt;/h2&gt;

&lt;p&gt;SketchUp's official SDK is a real, functional way to read and write &lt;code&gt;.skp&lt;/code&gt; files — if your project can tolerate everything that comes with it: a native dependency tied to specific platforms, a license that constrains how and where you can ship, and no path at all if you're building for Linux servers, WebAssembly, or anywhere the SDK simply isn't available.&lt;/p&gt;

&lt;p&gt;That constraint is exactly what two of the projects OpenSKP now powers ran into. FrameSmart, a 3D collaboration platform, needed to parse SketchUp files as part of a Linux-hosted pipeline. IngeTrazo, a Linux-first 3D modeler for civil engineering, was running the real SketchUp SDK through Wine — a working setup, but a fragile one, dragging a Windows-only dependency into a project that had no other reason to need it.&lt;/p&gt;

&lt;p&gt;Neither of those is an unusual situation. Anyone building a pipeline tool, a headless converter, a web-based viewer, or a CI step that touches SketchUp files runs into the same wall: the only "real" way in is a native SDK that assumes you're building a desktop plugin on Windows or macOS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reverse-engineering, not guessing
&lt;/h2&gt;

&lt;p&gt;Without a spec, the only way to understand the format is to look at real bytes and figure out what they mean — and then &lt;em&gt;prove&lt;/em&gt; the theory rather than just believing it looks plausible. That distinction matters more than it sounds. A parser that produces geometry that &lt;em&gt;looks&lt;/em&gt; right in a debug print is not the same as a parser that's actually correct; SketchUp files are dense binary structures where a single misread flag byte can silently corrupt geometry without ever throwing an error.&lt;/p&gt;

&lt;p&gt;The methodology that held up: build real files with the actual SketchUp application (or, once OpenSKP's own writer existed, the real SDK as a validation oracle), then diff OpenSKP's understanding of those files against what SketchUp itself reports through its own API — material colors, transparency values, transform matrices, vertex positions, all checked field by field rather than assumed. Several real bugs were only caught this way: a legacy-format alpha byte that four of the five language ports were silently discarding, a face's texture-positioning data that was being parsed but never linked back to the face it belonged to, a slot-numbering edge case that corrupted any file crossing a specific size threshold. None of those would have shown up in a "does it produce a mesh" smoke test. All of them showed up the moment real SketchUp was used as the source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two container formats, not one
&lt;/h2&gt;

&lt;p&gt;Part of what makes this format genuinely hard is that it isn't one format — it's two. SketchUp changed its internal container completely in the 2021 release. Files from SketchUp 2021 onward use &lt;strong&gt;VFF&lt;/strong&gt;, a ZIP-based container wrapping a TLV (Tag-Length-Value) binary tree. Files from SketchUp 2013–2020 use something structurally unrelated: a classic MFC &lt;code&gt;CArchive&lt;/code&gt; object-graph stream, with its own class-reference and back-reference numbering scheme, no ZIP involved at all.&lt;/p&gt;

&lt;p&gt;A tool that only reads one of these covers a shrinking slice of the real files people actually have sitting on disk — architecture firms, civil engineering practices, and product designers routinely have SketchUp libraries stretching back a decade. OpenSKP reads both, transparently, behind the same &lt;code&gt;parse()&lt;/code&gt; call in every language, which is a meaningfully larger reverse-engineering effort than picking the newer, better-documented-by-inference format and calling it done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why five languages, and why not bindings
&lt;/h2&gt;

&lt;p&gt;OpenSKP isn't a core parser in one language with thin wrapper bindings for the rest. Python, TypeScript, .NET, Dart, and C++ are five independent implementations of the same reverse-engineered format, each idiomatic to its own ecosystem — because a Python native extension is a poor fit for a browser-based TypeScript viewer, and a JavaScript parser is a poor fit for a native C++ desktop tool.&lt;/p&gt;

&lt;p&gt;The tradeoff is real: five implementations mean five places a bug can hide, and cross-language parity has to be actively maintained rather than assumed. In practice that means every non-trivial fix gets checked against all five ports' actual source before being called complete, and the same real &lt;code&gt;.skp&lt;/code&gt; fixtures get run through every language to confirm they produce identical geometry, layers, and materials — not just "each one compiles and returns something."&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stands now
&lt;/h2&gt;

&lt;p&gt;What started as a read-only reverse-engineering project has grown into a full toolkit: parsing both container formats, converting to seven other formats natively (glTF, OBJ, STL, PLY, DXF, IFC4, JSON), and — as of the 1.1.0 release — writing genuinely new &lt;code&gt;.skp&lt;/code&gt; files from scratch, in all five languages, with no SketchUp SDK involved at any point.&lt;/p&gt;

&lt;p&gt;None of it required Trimble's permission, a license fee, or a Windows machine. That was always the point.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;OpenSKP is open source under the MIT license: &lt;a href="https://github.com/iamahsanmehmood/openskp" rel="noopener noreferrer"&gt;github.com/iamahsanmehmood/openskp&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>software</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>How We Reverse-Engineered the SketchUp (.skp) Binary Format to Build a Pure JS/Python 3D Viewer</title>
      <dc:creator>Ahsan Mehmood</dc:creator>
      <pubDate>Wed, 24 Jun 2026 08:27:26 +0000</pubDate>
      <link>https://dev.to/iamahsanmehmood/how-we-reverse-engineered-the-sketchup-skp-binary-format-to-build-a-pure-jspython-3d-viewer-4nm2</link>
      <guid>https://dev.to/iamahsanmehmood/how-we-reverse-engineered-the-sketchup-skp-binary-format-to-build-a-pure-jspython-3d-viewer-4nm2</guid>
      <description>&lt;p&gt;For years, if you wanted to build a web-based 3D configurator, a mobile app, or a server-side pipeline that processed SketchUp (&lt;code&gt;.skp&lt;/code&gt;) files, you had one choice: integrate Trimble’s official, closed-source C++ SDK. &lt;/p&gt;

&lt;p&gt;This meant managing heavy native binaries, dealing with cross-platform compiling issues, and complying with proprietary licenses. &lt;/p&gt;

&lt;p&gt;We wanted to change that. Today, we are open-sourcing &lt;strong&gt;OpenSKP&lt;/strong&gt; — the world's first pure, multi-language binary parser and WebGL viewer for modern SketchUp (v2021+) files. It runs natively in Python, TypeScript, Dart, and .NET with &lt;strong&gt;zero native dependencies&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is how we reverse-engineered the format and built the rendering engine.&lt;/p&gt;




&lt;h2&gt;
  
  
  📁 Dissecting the SketchUp VFF Container
&lt;/h2&gt;

&lt;p&gt;Starting in SketchUp 2021, the file structure changed from a legacy OLE compound document to a &lt;strong&gt;Versioned File Format (VFF)&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Under the hood, a modern &lt;code&gt;.skp&lt;/code&gt; file is actually a ZIP archive disguised with a custom header. &lt;/p&gt;

&lt;p&gt;If you read the first few bytes of a &lt;code&gt;.skp&lt;/code&gt; file, you will find:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A 4-byte VFF magic number (&lt;code&gt;FF FE FF 0E&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;A UTF-16 encoded version string indicating the SketchUp build (e.g., &lt;code&gt;{24.0.594}&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;A local ZIP file marker (&lt;code&gt;PK\x03\x04&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By finding the byte offset of the &lt;code&gt;PK&lt;/code&gt; marker, we can seek directly to it and extract the ZIP contents natively in memory using standard libraries (&lt;code&gt;zipfile&lt;/code&gt; in Python or &lt;code&gt;fflate&lt;/code&gt; in JavaScript).&lt;/p&gt;

&lt;p&gt;Inside the zip, we find:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;model.dat&lt;/code&gt;: The core binary buffer containing all 3D geometry and instance nodes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;materials/&lt;/code&gt;: Folder containing XML files defining colors and opacity for each material.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;meta/model_thumbnail.png&lt;/code&gt;: A pre-rendered PNG preview of the model.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🌳 Parsing the TLV (Tag-Length-Value) Tree
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;model.dat&lt;/code&gt; file is encoded using a binary &lt;strong&gt;TLV (Tag-Length-Value)&lt;/strong&gt; schema. Every node in the tree starts with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tag&lt;/strong&gt; (2 bytes): Indicates the entity type (in hex).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Length&lt;/strong&gt; (4 bytes): Indicates the size of the payload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Value&lt;/strong&gt; (variable bytes): The payload itself or child TLV nodes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To parse it, we recursively traverse the buffer starting at offset &lt;code&gt;0&lt;/code&gt;. Here are the key tags we reverse-engineered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;F601&lt;/code&gt;: Root model container&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;7C15&lt;/code&gt;: Component definition (defining reusable 3D geometries)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;6419&lt;/code&gt;: Component instance (placing a definition in 3D space)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;C409&lt;/code&gt;: 3D Vertex (coordinate payloads)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;B80B&lt;/code&gt;: Edge (linking two vertices)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AC0D&lt;/code&gt;: Face (linking edge loops and defining normals)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;D007&lt;/code&gt; &amp;amp; &lt;code&gt;D207&lt;/code&gt;: Component metadata and Layer/Tag assignments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the core recursive TLV parser implemented in Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse_tlv_recursive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;container_tags&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;
    &lt;span class="n"&gt;elements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;tag_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpack_from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="n"&gt;tag_hex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tag_bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;children&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag_hex&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;container_tags&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;children&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_tlv_recursive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;container_tags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tag&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tag_hex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;size&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;children&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;payload&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;children&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;''&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;elements&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  📐 3D Planar Triangulation
&lt;/h2&gt;

&lt;p&gt;SketchUp faces can be complex N-gon polygons containing outer borders and inner holes (like a wall with window cutouts). To render them in WebGL or export to GLB, we must triangulate these loops.&lt;/p&gt;

&lt;p&gt;Since the vertices are defined in 3D space, we:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Retrieve the face's outward-facing normal vector from the &lt;code&gt;AD0D&lt;/code&gt; tag.&lt;/li&gt;
&lt;li&gt;Project the 3D vertices onto a local 2D plane perpendicular to the normal.&lt;/li&gt;
&lt;li&gt;Perform a 2D polygon triangulation (handling holes) using libraries like Earcut (JS) or Shapely (Python).&lt;/li&gt;
&lt;li&gt;Map the resulting 2D triangle indices back to the original 3D vertex IDs.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🚀 Getting Started
&lt;/h2&gt;

&lt;p&gt;OpenSKP supports multiple environments natively. Here is how you can use it today:&lt;/p&gt;

&lt;h3&gt;
  
  
  Python 🐍 (Server-Side Pipelines)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;openskp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openskp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SkpFile&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openskp.export&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;glb&lt;/span&gt;

&lt;span class="c1"&gt;# Open and parse the model
&lt;/span&gt;&lt;span class="n"&gt;skp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SkpFile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;villa.skp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;skp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SketchUp Version: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total Layers: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Export directly to web-ready GLB
&lt;/span&gt;&lt;span class="n"&gt;glb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output.glb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  TypeScript / Browser 🌐 (Client-Side Rendering)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;openskp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;parseSkp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openskp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Parse array buffer locally in the browser&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FileReader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseSkp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Parsed Layers:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Render model._glbPrimitives directly into a Three.js scene!&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readAsArrayBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🏠 Production Battle-Tested
&lt;/h2&gt;

&lt;p&gt;OpenSKP is already running in production to power real-time 3D configurations for framing systems at &lt;a href="https://frame-smart.com/" rel="noopener noreferrer"&gt;Frame-Smart&lt;/a&gt;! &lt;/p&gt;

&lt;p&gt;Special thanks to &lt;strong&gt;Noor Ali Qureshi&lt;/strong&gt; (&lt;a href="https://github.com/nooraliqureshi" rel="noopener noreferrer"&gt;@nooraliqureshi&lt;/a&gt;) for contributing crucial parsing fixes that enabled seamless support across multiple legacy and modern SketchUp file versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⭐️ Contributions welcome!
&lt;/h2&gt;

&lt;p&gt;OpenSKP is licensed under the MIT License. If you want to contribute, check out the source code, open issues, or submit PRs on GitHub:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://github.com/iamahsanmehmood/openskp" rel="noopener noreferrer"&gt;https://github.com/iamahsanmehmood/openskp&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>python</category>
      <category>opensource</category>
      <category>sketchup</category>
    </item>
    <item>
      <title>I Built the First Deterministic Urdu Compound Word Detector — Here's Why It Took a Full Library to Get There</title>
      <dc:creator>Ahsan Mehmood</dc:creator>
      <pubDate>Mon, 27 Apr 2026 18:32:27 +0000</pubDate>
      <link>https://dev.to/iamahsanmehmood/i-built-the-first-deterministic-urdu-compound-word-detector-heres-why-it-took-a-full-library-to-2l1o</link>
      <guid>https://dev.to/iamahsanmehmood/i-built-the-first-deterministic-urdu-compound-word-detector-heres-why-it-took-a-full-library-to-2l1o</guid>
      <description>&lt;p&gt;Urdu is spoken by over 230 million people. It is the national language of Pakistan, one of the 22 scheduled languages of India, and the lingua franca of a diaspora spanning three continents. And yet, if you try to build Urdu software today — real software, not a toy — you will hit the same wall every other developer hit before you: the tools do not exist.&lt;/p&gt;

&lt;p&gt;I hit that wall building &lt;a href="https://hamaariurdu.com" rel="noopener noreferrer"&gt;HamaariUrdu&lt;/a&gt;, an Urdu language learning platform. This post is about what I built to fix it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The bugs that no library could fix
&lt;/h2&gt;

&lt;p&gt;I was not looking to build a library. I was looking to ship features. But the bugs kept piling up, and none of the available Urdu NLP libraries (UrduHack, URDUNLP, or anything else) could fix them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 1: Search returning zero results for words that are obviously in the database.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The database stored &lt;code&gt;ہے&lt;/code&gt; using the correct Urdu &lt;code&gt;ہ&lt;/code&gt; (U+06C1, Heh Goal). The user's keyboard typed Arabic &lt;code&gt;ه&lt;/code&gt; (U+0647, Heh). Both look &lt;strong&gt;completely identical&lt;/strong&gt; on screen in Naskh fonts. But &lt;code&gt;U+06C1 !== U+0647&lt;/code&gt;. Zero results. No error. No warning. Just silence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 2: String equality silently failing.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;قلم&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;قلم&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;// false — why?!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One of those strings was copied from Microsoft Word and contains an invisible ZWNJ (Zero Width Non-Joiner, U+200C) that Word inserts automatically. You cannot see it. Your editor does not show it. But the comparison fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 3: TinyMCE destroying Izafat.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In Urdu grammar, Izafat (اضافت) is a grammatical construction that links two words — like the English "of" but expressed as a marker on the first word. The marker is often an apostrophe-like character (U+2019, Right Single Quotation Mark).&lt;/p&gt;

&lt;p&gt;TinyMCE — a very popular rich text editor — silently converts U+2019 to &lt;code&gt;&amp;amp;rsquo;&lt;/code&gt; before saving. So a word like &lt;code&gt;کتابِ&lt;/code&gt; (with Kasra) or a phrase using Izafat apostrophe gets stored as an HTML entity. Every compound word lookup in the database then fails because the stored form doesn't match the queried form.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 4: Numbers overflowing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Urdu text frequently references South Asian scale: لاکھ (100,000), کروڑ (10,000,000), ارب (1,000,000,000). These are real everyday numbers in Pakistan — newspaper headlines, financial documents, government statistics.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Number.MAX_SAFE_INTEGER&lt;/code&gt; is 9,007,199,254,740,991. A single کھرب (1 trillion) value loses precision with &lt;code&gt;typeof number&lt;/code&gt;. JavaScript silently gives you the wrong answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 5: Sorting broken for every Urdu word list.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No database and no JavaScript runtime has native Urdu collation. The Urdu alphabet has 39 letters in a specific order that does not match either Unicode codepoint order or any Latin-derived collation. Every sorted word list was wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 6 — the worst one: Compound words destroying every downstream NLP task.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This one deserves its own section.&lt;/p&gt;




&lt;h2&gt;
  
  
  The compound word problem
&lt;/h2&gt;

&lt;p&gt;Urdu مرکب الفاظ (compound words) are multi-word expressions that function as &lt;strong&gt;a single semantic unit&lt;/strong&gt; but are written with &lt;strong&gt;spaces between their parts&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;کتاب خانہ  →  library  (کتاب = book, خانہ = place)
بے عزت     →  disrespectful  (بے = without, عزت = honor)
خوش قسمت  →  fortunate  (خوش = well, قسمت = fate)
علم و عمل  →  knowledge and practice  (fixed expression)
محنت مشقت →  hard work  (synonym compound)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A naive tokenizer sees spaces and splits them. The result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input:   "اس نے کتاب خانہ بنایا"
                ↑ ↑
         space between compound components

Wrong:   ['اس', 'نے', 'کتاب', 'خانہ', 'بنایا']
         (5 tokens — "library" is split into "book" + "place")

Right:   ['اس', 'نے', 'کتاب‌خانہ', 'بنایا']
         (4 tokens — "library" is one semantic unit)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The consequences ripple into every downstream NLP task:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;What breaks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Search&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;کتاب خانہ&lt;/code&gt; doesn't match &lt;code&gt;کتاب‌خانہ&lt;/code&gt; — zero results&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NER&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;امورِ خانہ داری&lt;/code&gt; (household affairs) split into 3 unrelated tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sentiment&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;بے عزت&lt;/code&gt; (disrespectful) vs &lt;code&gt;بے&lt;/code&gt; + &lt;code&gt;عزت&lt;/code&gt; — polarity lost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Translation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;رنگ برنگے&lt;/code&gt; (colorful) translated as "color" + unknown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Word count&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Every compound inflates the count with phantom tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Why this is genuinely hard
&lt;/h3&gt;

&lt;p&gt;Urdu compound words span &lt;strong&gt;four different morphological strategies simultaneously&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strategy 1 — Affix-based:&lt;/strong&gt; One word contains a known derivational morpheme (prefix or suffix):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;کتاب + خانہ   →  library   (خانہ = "place of" suffix)
بے + عزت      →  disrespectful  (بے = "without" prefix)  
خوش + قسمت   →  fortunate  (خوش = "well" prefix)
کتاب + داری   →  librarianship  (داری = "keeping" suffix)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Strategy 2 — Izafat:&lt;/strong&gt; A grammatical linking marker appears in the text, written or implied:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;کتابِ حسنہ    (the good book)  — Zer mark (◌ِ) on first word
روحِ رواں     (driving spirit) — Hamza-above (◌ٔ) marker
علم و عمل     (knowledge and practice) — Vav-e-atf (و) connector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Strategy 3 — Lexical:&lt;/strong&gt; Neither word is morphologically special. You simply have to &lt;em&gt;know&lt;/em&gt; these pairs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;محنت مشقت     (hard work — synonym compound)
رنگ برنگے     (colorful — echo compound)
صبر شکر       (patient gratitude — near-synonym pair)
انسائیکلوپیڈیا آف اسلام  (3-word fixed title)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Strategy 4 — Chains:&lt;/strong&gt; Three or more words where each link is independently valid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;امورِ خانہ داری  (household affairs — 3 words)
↑       ↑   ↑
izafat  affix  suffix

Decomposition:
امورِ + خانہ  →  izafat compound
خانہ + داری  →  affix compound
Merged:  امورِ خانہ داری  →  one 3-word compound
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No statistical model trained on general text reliably covers all four strategies. They operate at different linguistic levels and require different detection mechanisms.&lt;/p&gt;




&lt;h2&gt;
  
  
  The approach: three deterministic layers
&lt;/h2&gt;

&lt;p&gt;Every other Urdu compound detection library (where one even exists) treats this as a &lt;strong&gt;machine learning problem&lt;/strong&gt;. They feed training data into statistical models and hope the probabilities align.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Results change unpredictably between corpus versions&lt;/li&gt;
&lt;li&gt;You cannot explain &lt;em&gt;why&lt;/em&gt; a pair was or wasn't detected&lt;/li&gt;
&lt;li&gt;Edge cases (literary izafat, 3-word expressions, echo words) fail silently&lt;/li&gt;
&lt;li&gt;No deterministic guarantee across identical inputs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;urdu-tools takes the opposite approach.&lt;/strong&gt; Every detection is grounded in one of three verifiable, explainable rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Raw text
   │
   ├─► Layer 1 — Affix (UAWL)
   │       100+ known Urdu prefix/suffix morphemes
   │       خانہ  گاہ  پرست  بے  نا  خوش  شب  غم  …
   │
   ├─► Layer 2 — Izafat
   │       zer mark (◌ِ) · hamza-above (◌ٔ) · vav-e-atf (و)
   │       کتابِ حسنہ · روحِ رواں · علم و عمل
   │
   └─► Layer 3 — Lexicon
           3,262 root entries · N-word tails · greedy longest-match
           محنت مشقت · رنگ برنگے · انسائیکلوپیڈیا آف اسلام
               │
               └─► Span chaining
                       امورِ خانہ  +  خانہ داری  →  امورِ خانہ داری
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The same input always produces the same output, always with a reason.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the first open-source implementation of deterministic, multi-layer, N-gram Urdu compound detection in any language.&lt;/p&gt;




&lt;h2&gt;
  
  
  Introducing urdu-tools
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/iamahsanmehmood/urdu-tools" rel="noopener noreferrer"&gt;github.com/iamahsanmehmood/urdu-tools&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A production-quality, zero-dependency Urdu text processing library. Available for TypeScript/JavaScript and C#/.NET, with identical APIs in both.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @iamahsanmehmood/urdu-tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package UrduTools.Core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;392 tests passing. 85 C# tests. 90%+ coverage enforced in CI.&lt;/p&gt;




&lt;h2&gt;
  
  
  The compound detection API
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;detectCompounds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;joinCompounds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;splitCompounds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isCompound&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@iamahsanmehmood/urdu-tools/compound&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Detecting compounds
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Layer 1: Affix — خانہ is a known place-suffix&lt;/span&gt;
&lt;span class="nf"&gt;detectCompounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;کتاب خانہ بہت اچھا ہے&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → [{&lt;/span&gt;
&lt;span class="c1"&gt;//     text: 'کتاب خانہ',&lt;/span&gt;
&lt;span class="c1"&gt;//     type: 'affix',&lt;/span&gt;
&lt;span class="c1"&gt;//     components: ['کتاب', 'خانہ'],&lt;/span&gt;
&lt;span class="c1"&gt;//     start: 0,&lt;/span&gt;
&lt;span class="c1"&gt;//     end: 1&lt;/span&gt;
&lt;span class="c1"&gt;//   }]&lt;/span&gt;

&lt;span class="c1"&gt;// Layer 1: Affix — بے is a known privative prefix&lt;/span&gt;
&lt;span class="nf"&gt;detectCompounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;بے عزت آدمی نہیں چاہیے&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → [{ text: 'بے عزت', type: 'affix', components: ['بے', 'عزت'], ... }]&lt;/span&gt;

&lt;span class="c1"&gt;// Layer 2: Izafat — standalone و (vav-e-atf) between content words&lt;/span&gt;
&lt;span class="nf"&gt;detectCompounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;علم و عمل ضروری ہے&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → [{ text: 'علم و عمل', type: 'izafat', components: ['علم', 'و', 'عمل'], ... }]&lt;/span&gt;

&lt;span class="c1"&gt;// Layer 3: Lexicon — echo compound, neither word is an affix&lt;/span&gt;
&lt;span class="nf"&gt;detectCompounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;رنگ برنگے پھول کھلے ہیں&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → [{ text: 'رنگ برنگے', type: 'lexicon', components: ['رنگ', 'برنگے'], ... }]&lt;/span&gt;

&lt;span class="c1"&gt;// Lexicon: synonym compound&lt;/span&gt;
&lt;span class="nf"&gt;detectCompounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;محنت مشقت کے بغیر کامیابی نہیں&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → [{ text: 'محنت مشقت', type: 'lexicon', ... }]&lt;/span&gt;

&lt;span class="c1"&gt;// 3-word chain: izafat (zer on امورِ) + affix (داری suffix on خانہ)&lt;/span&gt;
&lt;span class="nf"&gt;detectCompounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;امورِ خانہ داری چلانا مشکل ہے&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → [{ text: 'امورِ خانہ داری', type: 'affix', components: ['امورِ', 'خانہ', 'داری'], ... }]&lt;/span&gt;

&lt;span class="c1"&gt;// 3-word lexicon entry: greedy longest-match wins over any 2-word overlap&lt;/span&gt;
&lt;span class="nf"&gt;detectCompounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;انسائیکلوپیڈیا آف اسلام کا حوالہ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → [{ text: 'انسائیکلوپیڈیا آف اسلام', type: 'lexicon', ... }]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The pipeline: join before tokenize
&lt;/h3&gt;

&lt;p&gt;The critical downstream use case — bind compounds &lt;em&gt;before&lt;/em&gt; tokenizing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;joinCompounds&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@iamahsanmehmood/urdu-tools/compound&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tokenize&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@iamahsanmehmood/urdu-tools&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;کتاب خانہ میں علم و عمل کی کتابیں ہیں&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// Without compound joining — naive tokenizer splits everything&lt;/span&gt;
&lt;span class="nf"&gt;tokenize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → ['کتاب', 'خانہ', 'میں', 'علم', 'و', 'عمل', 'کی', 'کتابیں', 'ہیں']&lt;/span&gt;
&lt;span class="c1"&gt;//    ↑ split!                 ↑ split!&lt;/span&gt;

&lt;span class="c1"&gt;// With compound joining — semantic integrity preserved&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;joined&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;joinCompounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → 'کتاب‌خانہ میں علم‌و‌عمل کی کتابیں ہیں'&lt;/span&gt;
&lt;span class="c1"&gt;//          ↑ ZWNJ (invisible, prevents tokenizer split)&lt;/span&gt;

&lt;span class="nf"&gt;tokenize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;joined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → ['کتاب‌خانہ', 'میں', 'علم‌و‌عمل', 'کی', 'کتابیں', 'ہیں']&lt;/span&gt;
&lt;span class="c1"&gt;//    ↑ one token            ↑ one token  ✓&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ZWNJ (Zero Width Non-Joiner, U+200C) is invisible but meaningful — the tokenizer sees it and keeps the word intact.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pair-level check
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;isCompound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;کتاب&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;خانہ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;// → { matched: true,  type: 'affix'   }&lt;/span&gt;
&lt;span class="nf"&gt;isCompound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;محنت&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;مشقت&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;// → { matched: true,  type: 'lexicon' }&lt;/span&gt;
&lt;span class="nf"&gt;isCompound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;اخلاقِ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;حسنہ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// → { matched: true,  type: 'izafat' }&lt;/span&gt;
&lt;span class="nf"&gt;isCompound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;اچھا&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;آدمی&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;// → { matched: false, type: null      }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Fine-grained control
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Use only specific layers&lt;/span&gt;
&lt;span class="nf"&gt;detectCompounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;affix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;izafat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lexicon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nf"&gt;detectCompounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;affix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;izafat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lexicon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Choose the binder character for joinCompounds&lt;/span&gt;
&lt;span class="nf"&gt;joinCompounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                      &lt;span class="c1"&gt;// ZWNJ U+200C (default, invisible)&lt;/span&gt;
&lt;span class="nf"&gt;joinCompounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;binder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nbsp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;  &lt;span class="c1"&gt;// Non-breaking space (visible)&lt;/span&gt;
&lt;span class="nf"&gt;joinCompounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;binder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wj&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;   &lt;span class="c1"&gt;// Word Joiner U+2060 (never line-breaks)&lt;/span&gt;

&lt;span class="c1"&gt;// Inverse — split back to spaces&lt;/span&gt;
&lt;span class="nf"&gt;splitCompounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;کتاب‌خانہ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// → 'کتاب خانہ'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The normalization pipeline
&lt;/h2&gt;

&lt;p&gt;A 12-layer normalization pipeline — the foundation that every other module builds on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fingerprint&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@iamahsanmehmood/urdu-tools&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 — NFC&lt;/td&gt;
&lt;td&gt;Unicode canonical form&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 — NBSP&lt;/td&gt;
&lt;td&gt;Non-breaking space → regular space&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 — Alif Madda&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;آ&lt;/code&gt; → &lt;code&gt;آ&lt;/code&gt; (precomposed)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 — Numerals&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;٠–٩&lt;/code&gt; and &lt;code&gt;۰–۹&lt;/code&gt; → ASCII &lt;code&gt;0–9&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 — Zero-width&lt;/td&gt;
&lt;td&gt;Strip ZWNJ, ZWJ, soft hyphen&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6 — Diacritics&lt;/td&gt;
&lt;td&gt;Strip zabar, zer, pesh, shadda, sukun, tanwin&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7 — Honorifics&lt;/td&gt;
&lt;td&gt;Strip Islamic honorific signs (ؐ ؑ ؒ ؓ ؔ)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8 — Hamza&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;أ&lt;/code&gt; → &lt;code&gt;ا&lt;/code&gt;, &lt;code&gt;ؤ&lt;/code&gt; → &lt;code&gt;و&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9 — Kashida&lt;/td&gt;
&lt;td&gt;Strip tatweel U+0640&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 — Presentation forms&lt;/td&gt;
&lt;td&gt;Map U+FB50–FEFF to base chars&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11 — Punctuation trim&lt;/td&gt;
&lt;td&gt;Strip leading/trailing non-letter chars&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12 — Char normalize&lt;/td&gt;
&lt;td&gt;Arabic look-alikes → correct Urdu codepoints&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;عِلمٌ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                    &lt;span class="c1"&gt;// 'علم'  (layers 1–6: diacritics stripped)&lt;/span&gt;
&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;آ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="c1"&gt;// 'آ'    (layer 3: Alif + Madda → precomposed)&lt;/span&gt;
&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;علم‌ہے&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="c1"&gt;// 'علمہے' (layer 5: ZWNJ stripped)&lt;/span&gt;
&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;نبیؐ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                    &lt;span class="c1"&gt;// 'نبی'  (layer 7: honorific stripped)&lt;/span&gt;

&lt;span class="c1"&gt;// Full normalization for search indexing&lt;/span&gt;
&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;kashida&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;presentationForms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;punctuationTrim&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;normalizeCharacters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// ي → ی, ك → ک, ه → ہ&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The fingerprint function
&lt;/h3&gt;

&lt;p&gt;For client-side word comparison without database round-trips:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;عِلمٌ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nf"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;عَلم&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// true (both normalize to 'علم')&lt;/span&gt;
&lt;span class="nf"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;نبیؐ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nf"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;نبی&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;// true (honorific stripped)&lt;/span&gt;
&lt;span class="nf"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;علم‌&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nf"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;علم&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// true (ZWNJ stripped)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We use this in HamaariUrdu to compare user input against stored words in a 110,000+ word dictionary without needing a round-trip to the database for every keystroke.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Arabic–Urdu confusion problem
&lt;/h2&gt;

&lt;p&gt;This is the &lt;strong&gt;single most common source of silent failures&lt;/strong&gt; in Urdu software, and no existing library addressed it.&lt;/p&gt;

&lt;p&gt;Three character pairs are &lt;strong&gt;visually identical&lt;/strong&gt; in Naskh fonts but are different Unicode code points:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Visual&lt;/th&gt;
&lt;th&gt;Arabic codepoint&lt;/th&gt;
&lt;th&gt;Urdu codepoint&lt;/th&gt;
&lt;th&gt;Common source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ی&lt;/td&gt;
&lt;td&gt;ي U+064A&lt;/td&gt;
&lt;td&gt;ی U+06CC&lt;/td&gt;
&lt;td&gt;Arabic-layout keyboards, Arabic websites&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ک&lt;/td&gt;
&lt;td&gt;ك U+0643&lt;/td&gt;
&lt;td&gt;ک U+06A9&lt;/td&gt;
&lt;td&gt;Arabic-layout keyboards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ہ&lt;/td&gt;
&lt;td&gt;ه U+0647&lt;/td&gt;
&lt;td&gt;ہ U+06C1&lt;/td&gt;
&lt;td&gt;Arabic text pasted into Urdu context&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A user searching for &lt;code&gt;ہے&lt;/code&gt; typed with Arabic &lt;code&gt;ه&lt;/code&gt; finds &lt;strong&gt;zero results&lt;/strong&gt; in a database that stored it with Urdu &lt;code&gt;ہ&lt;/code&gt;. Both look identical on screen. No error. No warning. Zero results.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;normalizeCharacters&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@iamahsanmehmood/urdu-tools&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;normalizeCharacters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ي&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// → 'ی'  (U+064A → U+06CC)&lt;/span&gt;
&lt;span class="nf"&gt;normalizeCharacters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ك&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// → 'ک'  (U+0643 → U+06A9)&lt;/span&gt;
&lt;span class="nf"&gt;normalizeCharacters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ه&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// → 'ہ'  (U+0647 → U+06C1)&lt;/span&gt;

&lt;span class="c1"&gt;// Apply before storage or search indexing:&lt;/span&gt;
&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;normalizeCharacters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Progressive search matching
&lt;/h2&gt;

&lt;p&gt;The search module tries 9 progressively aggressive normalization layers until it finds a match — or returns false with full diagnostic info.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fuzzyMatch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getAllNormalizations&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@iamahsanmehmood/urdu-tools&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;عِلمٌ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;علم&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → { matched: true, layer: 'strip-diacritics', normalizedQuery: 'علم', normalizedTarget: 'علم' }&lt;/span&gt;

&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;نبیؐ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;نبی&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → { matched: true, layer: 'strip-honorifics', ... }&lt;/span&gt;

&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;أحمد&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;احمد&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → { matched: true, layer: 'normalize-hamza', ... }&lt;/span&gt;

&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;کتاب&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;علم&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → { matched: false, layer: null, ... }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For database lookups, &lt;code&gt;getAllNormalizations()&lt;/code&gt; returns every normalized form to try:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;forms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAllNormalizations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;عِلمٌ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → ['عِلمٌ', 'عِلم', 'علم', ...]  (from most specific to most aggressive)&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;forms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fuzzy matching uses Levenshtein + LCS hybrid (threshold 0.5):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;fuzzyMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;کتاب&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;کتابیں&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;کتب&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;علم&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="c1"&gt;// → { candidate: 'کتابیں', score: ~0.7 }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Numbers — South Asian scale with bigint
&lt;/h2&gt;

&lt;p&gt;The South Asian number system has named units that don't exist in Western mathematics:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Urdu&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ہزار&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;لاکھ&lt;/td&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;کروڑ&lt;/td&gt;
&lt;td&gt;10,000,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ارب&lt;/td&gt;
&lt;td&gt;1,000,000,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;کھرب&lt;/td&gt;
&lt;td&gt;1,000,000,000,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;نیل&lt;/td&gt;
&lt;td&gt;1,000,000,000,000,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The entire module uses &lt;code&gt;bigint&lt;/code&gt; throughout — South Asian numbers exceed &lt;code&gt;Number.MAX_SAFE_INTEGER&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;numberToWords&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;formatCurrency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toUrduNumerals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wordsToNumber&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@iamahsanmehmood/urdu-tools&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;numberToWords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                      &lt;span class="c1"&gt;// 'صفر'&lt;/span&gt;
&lt;span class="nf"&gt;numberToWords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                    &lt;span class="c1"&gt;// 'ایک سو'&lt;/span&gt;
&lt;span class="nf"&gt;numberToWords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="nx"&gt;_000n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                &lt;span class="c1"&gt;// 'ایک لاکھ'&lt;/span&gt;
&lt;span class="nf"&gt;numberToWords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000_000n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="c1"&gt;// 'ایک کروڑ'&lt;/span&gt;
&lt;span class="nf"&gt;numberToWords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000_000_000_000n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// 'ایک نیل'&lt;/span&gt;

&lt;span class="c1"&gt;// Ordinals with gender agreement&lt;/span&gt;
&lt;span class="nf"&gt;numberToWords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ordinal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;gender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;masculine&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;  &lt;span class="c1"&gt;// 'پہلا'&lt;/span&gt;
&lt;span class="nf"&gt;numberToWords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ordinal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;gender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;feminine&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;   &lt;span class="c1"&gt;// 'پہلی'&lt;/span&gt;
&lt;span class="nf"&gt;numberToWords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ordinal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;gender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;masculine&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="c1"&gt;// 'گیارہواں'&lt;/span&gt;
&lt;span class="nf"&gt;numberToWords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ordinal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;gender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;feminine&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;  &lt;span class="c1"&gt;// 'گیارہویں'&lt;/span&gt;

&lt;span class="c1"&gt;// Currency&lt;/span&gt;
&lt;span class="nf"&gt;formatCurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;505.50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PKR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// 'پانچ سو پانچ روپے پچاس پیسے'&lt;/span&gt;
&lt;span class="nf"&gt;formatCurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;// 'ایک ہزار روپے'&lt;/span&gt;

&lt;span class="c1"&gt;// Numeral conversion&lt;/span&gt;
&lt;span class="nf"&gt;toUrduNumerals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;// '۲۰۲۴'&lt;/span&gt;

&lt;span class="c1"&gt;// Inverse — parse words back to number&lt;/span&gt;
&lt;span class="nf"&gt;wordsToNumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ایک کروڑ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;// 10_000_000n&lt;/span&gt;
&lt;span class="nf"&gt;wordsToNumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;پانچ سو پانچ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// 505n&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Canonical Urdu sorting
&lt;/h2&gt;

&lt;p&gt;No database and no JavaScript runtime has native Urdu collation. The 39-letter Urdu alphabet order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ء ا ب پ ت ٹ ث ج چ ح خ د ڈ ذ ر ڑ ز ژ س ش ص ض ط ظ ع غ ف ق ک گ ل م ن ں و ہ ھ ی ے
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;compare&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sortKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@iamahsanmehmood/urdu-tools&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ے&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ا&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ک&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ب&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;           &lt;span class="c1"&gt;// → ['ا', 'ب', 'ک', 'ے']&lt;/span&gt;
&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;زبان&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;اردو&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;بہترین&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;      &lt;span class="c1"&gt;// → ['اردو', 'بہترین', 'زبان']&lt;/span&gt;

&lt;span class="c1"&gt;// Use compare() as a comparator for any sorting context&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ے&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ا&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ک&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;compare&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;// → ['ا', 'ک', 'ے']&lt;/span&gt;

&lt;span class="c1"&gt;// sortKey() for indexing — diacritics stripped before key generation&lt;/span&gt;
&lt;span class="nf"&gt;sortKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;پاکستان&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// '030003091102280814'&lt;/span&gt;
&lt;span class="c1"&gt;// عِلم and عَلم sort to the same position&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In C# it implements &lt;code&gt;IComparer&amp;lt;string&amp;gt;&lt;/code&gt; for native LINQ integration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UrduTools.Core.Sorting&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;words&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"ے"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ا"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ک"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ب"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sorted&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UrduComparer&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// ["ا", "ب", "ک", "ے"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Unicode-aware tokenization
&lt;/h2&gt;

&lt;p&gt;The tokenizer handles the edge cases that matter in real Urdu text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tokenize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sentences&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ngrams&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@iamahsanmehmood/urdu-tools&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;tokenize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;پاکستان ایک خوبصورت ملک ہے&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → [&lt;/span&gt;
&lt;span class="c1"&gt;//   { text: 'پاکستان', type: 'urdu-word' },&lt;/span&gt;
&lt;span class="c1"&gt;//   { text: 'ایک',     type: 'urdu-word' },&lt;/span&gt;
&lt;span class="c1"&gt;//   { text: 'خوبصورت', type: 'urdu-word' },&lt;/span&gt;
&lt;span class="c1"&gt;//   { text: 'ملک',     type: 'urdu-word' },&lt;/span&gt;
&lt;span class="c1"&gt;//   { text: 'ہے',      type: 'urdu-word' },&lt;/span&gt;
&lt;span class="c1"&gt;// ]&lt;/span&gt;

&lt;span class="c1"&gt;// Sentence splitting — on ۔ (U+06D4) ؟ ! but NOT on ، or ؛&lt;/span&gt;
&lt;span class="nf"&gt;sentences&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;پہلا جملہ۔ دوسرا جملہ؟ تیسرا جملہ!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → ['پہلا جملہ', 'دوسرا جملہ', 'تیسرا جملہ']&lt;/span&gt;

&lt;span class="c1"&gt;// The tokenizer preserves ZWNJ within words —&lt;/span&gt;
&lt;span class="c1"&gt;// so joinCompounds() output is one token per compound&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key edge cases handled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Izafat Kasra (U+0650) at word boundaries is not treated as a split point&lt;/li&gt;
&lt;li&gt;ZWNJ-bound compounds (output of &lt;code&gt;joinCompounds()&lt;/code&gt;) are kept as single tokens&lt;/li&gt;
&lt;li&gt;Mixed Urdu/Latin text is classified per-token&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Transliteration — 18 aspirated digraphs
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;toRoman&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fromRoman&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@iamahsanmehmood/urdu-tools&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;toRoman&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;پاکستان&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// 'pakistan'&lt;/span&gt;
&lt;span class="nf"&gt;toRoman&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;بھارت&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;// 'bharat'&lt;/span&gt;
&lt;span class="nf"&gt;toRoman&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;چھوٹا&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;// 'chhota'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Digraph rules (left-to-right FSM, digraph priority):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Urdu&lt;/th&gt;
&lt;th&gt;Roman&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Urdu&lt;/th&gt;
&lt;th&gt;Roman&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;بھ&lt;/td&gt;
&lt;td&gt;bh&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;پھ&lt;/td&gt;
&lt;td&gt;ph&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;تھ&lt;/td&gt;
&lt;td&gt;th&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ٹھ&lt;/td&gt;
&lt;td&gt;Th&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;جھ&lt;/td&gt;
&lt;td&gt;jh&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;چھ&lt;/td&gt;
&lt;td&gt;chh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;دھ&lt;/td&gt;
&lt;td&gt;dh&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ڈھ&lt;/td&gt;
&lt;td&gt;Dh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;کھ&lt;/td&gt;
&lt;td&gt;kh&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;گھ&lt;/td&gt;
&lt;td&gt;gh&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;fromRoman&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pakistan&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// → 'پاکستان' (trie-based longest-prefix match)&lt;/span&gt;
&lt;span class="nf"&gt;fromRoman&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bharat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;// → 'بھارت'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  InPage encoding — decoding 30 years of Urdu archives
&lt;/h2&gt;

&lt;p&gt;InPage was the dominant Urdu desktop publishing tool for decades. Millions of documents — newspapers, books, government archives — exist only in InPage format. The library decodes all three versions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;decodeInpage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;detectEncoding&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@iamahsanmehmood/urdu-tools&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// Auto-detect InPage version and decode&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decodeInpage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// result.paragraphs → string[]  (Unicode Urdu text)&lt;/span&gt;
&lt;span class="c1"&gt;// result.version   → 'v1' | 'v2' | 'v3'&lt;/span&gt;

&lt;span class="c1"&gt;// Explicit version&lt;/span&gt;
&lt;span class="nf"&gt;decodeInpage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// 0x04-prefix byte-pair encoding (old InPage)&lt;/span&gt;
&lt;span class="nf"&gt;decodeInpage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// UTF-16LE with paragraph markers&lt;/span&gt;

&lt;span class="c1"&gt;// Detect encoding from buffer alone&lt;/span&gt;
&lt;span class="nf"&gt;detectEncoding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// → 'utf-8' | 'utf-16le' | 'windows-1256' | 'inpage-v1v2' | 'inpage-v3' | 'unknown'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  String utilities
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;truncate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wordCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;charCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="nx"&gt;extractUrdu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;decodeHtmlEntities&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@iamahsanmehmood/urdu-tools&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// Reverse word order (not characters — preserves Arabic shaping)&lt;/span&gt;
&lt;span class="nf"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;پاکستان ہندوستان&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;// → 'ہندوستان پاکستان'&lt;/span&gt;

&lt;span class="c1"&gt;// Truncate at word boundary&lt;/span&gt;
&lt;span class="nf"&gt;truncate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;یہ ایک بہت لمبا جملہ ہے&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// → 'یہ ایک...'&lt;/span&gt;

&lt;span class="c1"&gt;// Count grapheme clusters (correct for combining diacritics)&lt;/span&gt;
&lt;span class="nf"&gt;charCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;عِلم&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// → 3  (ع+ِ = 1 cluster, ل, م)&lt;/span&gt;

&lt;span class="c1"&gt;// Extract Urdu/Arabic segments from mixed text&lt;/span&gt;
&lt;span class="nf"&gt;extractUrdu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The word علم means knowledge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// → ['علم']&lt;/span&gt;

&lt;span class="c1"&gt;// Decode HTML entities BEFORE normalize() — critical for TinyMCE/Quill content&lt;/span&gt;
&lt;span class="nf"&gt;decodeHtmlEntities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;کتاب&amp;amp;rsquo;خانہ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// → 'کتاب’خانہ'&lt;/span&gt;
&lt;span class="nf"&gt;decodeHtmlEntities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;علم&amp;amp;nbsp;ہے&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;// → 'علم&amp;nbsp;ہے'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last one (&lt;code&gt;decodeHtmlEntities&lt;/code&gt;) is the fix for the TinyMCE bug mentioned at the top. Always call it before normalizing text that came from a rich text editor.&lt;/p&gt;




&lt;h2&gt;
  
  
  Script and character analysis
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isUrduChar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getScript&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;classifyChar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isRTL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getUrduDensity&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@iamahsanmehmood/urdu-tools&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;isUrduChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;پ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// true  — U+067E is Urdu-specific&lt;/span&gt;
&lt;span class="nf"&gt;isUrduChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ب&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// false — U+0628 is shared with Arabic&lt;/span&gt;
&lt;span class="nf"&gt;isUrduChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;۱&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// true  — U+06F1 Urdu numeral&lt;/span&gt;

&lt;span class="nf"&gt;getScript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;پاکستان&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;// 'urdu'&lt;/span&gt;
&lt;span class="nf"&gt;getScript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;مرحبا&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="c1"&gt;// 'arabic'&lt;/span&gt;
&lt;span class="nf"&gt;getScript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello پاکستان&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;// 'mixed'&lt;/span&gt;

&lt;span class="nf"&gt;classifyChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;پ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// 'urdu-letter'&lt;/span&gt;
&lt;span class="nf"&gt;classifyChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;َ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// 'diacritic'&lt;/span&gt;
&lt;span class="nf"&gt;classifyChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;۱&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// 'numeral'&lt;/span&gt;

&lt;span class="nf"&gt;isRTL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;پاکستان&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="nf"&gt;getUrduDensity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;پاکستان زندہ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// 0.28&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  C#/.NET — identical API, zero dependencies
&lt;/h2&gt;

&lt;p&gt;Every function is available in &lt;code&gt;UrduTools.Core&lt;/code&gt; with the same behavior. The C# package mirrors the TypeScript structure exactly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UrduTools.Core.Normalization&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UrduTools.Core.Compound&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UrduTools.Core.Numbers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UrduTools.Core.Sorting&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UrduTools.Core.Search&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Normalize&lt;/span&gt;
&lt;span class="n"&gt;UrduNormalizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"عِلمٌ"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                          &lt;span class="c1"&gt;// "علم"&lt;/span&gt;
&lt;span class="n"&gt;UrduNormalizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"علم‌"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                      &lt;span class="c1"&gt;// "علم"&lt;/span&gt;

&lt;span class="c1"&gt;// Compound detection&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;spans&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CompoundDetector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DetectCompounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"کتاب خانہ میں"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// spans[0].Text == "کتاب خانہ"&lt;/span&gt;
&lt;span class="c1"&gt;// spans[0].Type == CompoundType.Affix&lt;/span&gt;

&lt;span class="c1"&gt;// Numbers&lt;/span&gt;
&lt;span class="n"&gt;NumberToWords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10_000_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// "ایک کروڑ"&lt;/span&gt;
&lt;span class="n"&gt;NumberToWords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;NumberOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Ordinal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Gender&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Gender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Feminine&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;  &lt;span class="c1"&gt;// "پہلی"&lt;/span&gt;

&lt;span class="c1"&gt;// Sort&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sorted&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"ے"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ا"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ک"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ب"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UrduComparer&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// ["ا", "ب", "ک", "ے"]&lt;/span&gt;

&lt;span class="c1"&gt;// Progressive normalization for DB lookup&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;UrduMatcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAllNormalizations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userInput&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LookupAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Match&lt;/span&gt;
&lt;span class="n"&gt;UrduMatcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"عِلمٌ"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"علم"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Matched&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// true, layer: StripDiacritics&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Academic foundation
&lt;/h2&gt;

&lt;p&gt;The compound word detection module was built on peer-reviewed Urdu linguistics research. These three works directly informed the architecture:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Jabbar, A. (2016). "Urdu Compound Words Manufacturing a State of Art."&lt;/strong&gt;&lt;br&gt;
Provides the Urdu Affix Word List (UAWL) — the definitive catalog of Urdu derivational morphemes. The 100+ affix morphemes in Layer 1 (&lt;code&gt;AFFIX_SET&lt;/code&gt;, &lt;code&gt;PREFIX_SET&lt;/code&gt;, &lt;code&gt;SUFFIX_SET&lt;/code&gt;) are drawn from this work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rahman, M. "A Linguistic Classification of Urdu Compound Words."&lt;/strong&gt;&lt;br&gt;
Informed the typological distinctions between compound categories — specifically the Perso-Arabic vs. native Urdu origin split and vav-e-atf chain patterns. Shaped the &lt;code&gt;CompoundType&lt;/code&gt; taxonomy and izafat heuristics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"High Performance Stemming Algorithm to Handle Multi-Word Expressions."&lt;/strong&gt;&lt;br&gt;
Motivated the &lt;code&gt;joinCompounds()&lt;/code&gt; + &lt;code&gt;tokenize()&lt;/code&gt; pipeline design — the paper demonstrates that semantic integrity is best preserved by preventing erroneous splits at the input boundary, not by post-processing token sequences. Also reinforced N-gram scanning over bigram-only approaches.&lt;/p&gt;


&lt;h2&gt;
  
  
  Used in production
&lt;/h2&gt;

&lt;p&gt;This library is not a side project. It runs in three production systems:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://hamaariurdu.com" rel="noopener noreferrer"&gt;HamaariUrdu&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Urdu language learning platform — normalization, search, compound detection, numbers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://pal.gov.pk" rel="noopener noreferrer"&gt;Pakistan Academy of Letters&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Government literary institution — normalization, search, sorting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://dlp.gov.pk" rel="noopener noreferrer"&gt;Digital Library of PAL&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Government digital Urdu archive — normalization, search, encoding&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;HamaariUrdu was the origin — the library was extracted from production code where these problems were first encountered and solved. PAL and DLP integrated later for their Urdu text search and archiving systems.&lt;/p&gt;


&lt;h2&gt;
  
  
  Live Playground
&lt;/h2&gt;

&lt;p&gt;Every function is interactive at &lt;strong&gt;&lt;a href="https://iamahsanmehmood.github.io/urdu-tools/" rel="noopener noreferrer"&gt;iamahsanmehmood.github.io/urdu-tools&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The playground includes compound reporting built-in: if you find a compound the detector misses, or a pair it wrongly detects, you can report it directly from the UI — a pre-filled GitHub issue opens in one click.&lt;/p&gt;


&lt;h2&gt;
  
  
  Contributing
&lt;/h2&gt;

&lt;p&gt;The compound lexicon (3,262 roots, expandable) is the highest-impact area for non-developer contributions. If you know Urdu, you can contribute without writing code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/urdu-js/src/compound/lexicon-data.ts&lt;/span&gt;
&lt;span class="c1"&gt;// Format: ['rootWord', new Set(['tail1', 'tail2'])]&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;محنت&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;مشقت&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])],&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;علم&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;و ہنر&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;و عمل&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;کیمیا&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])],&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;انسائیکلوپیڈیا&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;آف اسلام&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full guide in &lt;a href="https://github.com/iamahsanmehmood/urdu-tools/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;strong&gt;&lt;a href="https://github.com/iamahsanmehmood/urdu-tools" rel="noopener noreferrer"&gt;github.com/iamahsanmehmood/urdu-tools&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;اردو سافٹ ویئر کو بہتر بنانے میں ہمارا ساتھ دیں۔&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Help us make Urdu software better.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: #urdu #nlp #typescript #dotnet #opensource&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nlp</category>
      <category>opensource</category>
      <category>typescript</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Building a Multi-Terminal Restaurant POS with C# .NET — Architecture &amp; Lessons</title>
      <dc:creator>Ahsan Mehmood</dc:creator>
      <pubDate>Mon, 09 Mar 2026 03:33:29 +0000</pubDate>
      <link>https://dev.to/iamahsanmehmood/building-a-multi-terminal-restaurant-pos-with-c-net-architecture-lessons-56e2</link>
      <guid>https://dev.to/iamahsanmehmood/building-a-multi-terminal-restaurant-pos-with-c-net-architecture-lessons-56e2</guid>
      <description>&lt;p&gt;Building a Point-of-Sale system sounds straightforward until you realize it needs to handle &lt;strong&gt;multiple terminals, thermal printers, kitchen displays, real-time table tracking, and never lose a transaction&lt;/strong&gt; — even when the network drops.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;RestoCare+&lt;/strong&gt; — a multi-terminal restaurant POS system — and in this post, I'll walk through the architecture decisions, the problems I ran into, and what I'd do differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;I spent 4+ years working as a supervisor at a restaurant in Islamabad. I saw firsthand how terrible most POS systems were — slow, unreliable, confusing for staff, and impossible to maintain.&lt;/p&gt;

&lt;p&gt;When I transitioned into software development, this was the first real product I wanted to build. Not because it was technically exciting, but because I &lt;strong&gt;deeply understood the problem&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Language:&lt;/strong&gt; C#&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework:&lt;/strong&gt; .NET Framework (WinForms for UI)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database:&lt;/strong&gt; SQL Server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Printing:&lt;/strong&gt; ESC/POS commands via Windows Services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture:&lt;/strong&gt; Client-Server with centralized SQL database&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  High-Level Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│  Terminal 1   │    │  Terminal 2   │    │  Terminal 3   │
│  (Cashier)    │    │  (Cashier)    │    │  (Manager)    │
└──────┬───────┘    └──────┬───────┘    └──────┬───────┘
       │                   │                   │
       └───────────┬───────┴───────────────────┘
                   │
           ┌───────┴───────┐
           │   SQL Server   │
           │   (Central DB) │
           └───────┬───────┘
                   │
       ┌───────────┼───────────┐
       │           │           │
┌──────┴──┐  ┌────┴────┐  ┌───┴──────┐
│ Thermal  │  │ Kitchen  │  │ Receipt  │
│ Printer 1│  │ Display  │  │ Printer  │
└─────────┘  └─────────┘  └──────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Design Decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Centralized Database, Not Peer-to-Peer
&lt;/h3&gt;

&lt;p&gt;Every terminal connects to a &lt;strong&gt;single SQL Server instance&lt;/strong&gt;. I considered SQLite per terminal with sync, but restaurants can't tolerate eventual consistency — if Terminal 1 marks Table 5 as occupied, Terminal 2 needs to see that immediately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Connection string points to central SQL Server&lt;/span&gt;
&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;connectionString&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ConfigurationManager&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConnectionStrings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"RestoCareDB"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;ConnectionString&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Real-Time Table Management
&lt;/h3&gt;

&lt;p&gt;The table management system uses a polling approach to keep all terminals in sync. Every terminal refreshes the floor plan every few seconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Table status refresh timer&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Timer&lt;/span&gt; &lt;span class="n"&gt;_tableRefreshTimer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;InitializeTableSync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_tableRefreshTimer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Timer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 3-second interval&lt;/span&gt;
    &lt;span class="n"&gt;_tableRefreshTimer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Elapsed&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;RefreshTableStatuses&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;_tableRefreshTimer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;RefreshTableStatuses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ElapsedEventArgs&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_tableRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAllWithStatus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;UpdateTableUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why polling instead of SignalR/WebSockets?&lt;/strong&gt; Simplicity. In a restaurant with 3-5 terminals on a local network, a 3-second poll is good enough and dramatically simpler to debug when something goes wrong at 9 PM on a Friday night.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Thermal Printing via Windows Service
&lt;/h3&gt;

&lt;p&gt;Thermal receipt printers speak &lt;strong&gt;ESC/POS&lt;/strong&gt; — a binary command language. I built a dedicated Windows Service (&lt;code&gt;IMS Print Service&lt;/code&gt;) that handles all print jobs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ESC/POS command to print bold text&lt;/span&gt;
&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;boldOn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="m"&gt;0x1B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0x45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0x01&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;boldOff&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="m"&gt;0x1B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0x45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0x00&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;centerAlign&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="m"&gt;0x1B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0x61&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0x01&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;PrintReceipt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;printer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RawPrinter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_printerName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;printer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;centerAlign&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;printer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;boldOn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;printer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"RESTAURANT NAME\n"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;printer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;boldOff&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;printer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,-&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; x&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;\n"&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;printer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"\nTOTAL: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;\n"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;printer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CutPaper&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why a Windows Service?&lt;/strong&gt; Two reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The print service runs independently — even if the POS UI crashes, queued prints still go through.&lt;/li&gt;
&lt;li&gt;Multiple terminals can send print jobs to the same service, which queues them to avoid conflicts.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  4. Kitchen Display Integration
&lt;/h3&gt;

&lt;p&gt;When a waiter submits an order, it needs to appear on the kitchen display instantly. The kitchen display is a separate WinForms app running on a screen in the kitchen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Kitchen display polls for new orders&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;KitchenOrder&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetPendingOrders&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RestoCareContext&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pending&lt;/span&gt; 
                     &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InProgress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kitchen staff tap items as they're prepared, and the waiter's terminal updates in real-time showing which items are ready.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Handling the Offline Scenario
&lt;/h3&gt;

&lt;p&gt;What happens when the network drops? This is critical in restaurants — you can't stop taking orders because WiFi went down.&lt;/p&gt;

&lt;p&gt;My approach: &lt;strong&gt;local queue with retry.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// If SQL Server is unreachable, queue locally&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SubmitOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_orderRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_kitchenService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotifyNewOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SqlException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_localQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DB unreachable — order queued locally"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Background task retries every 10 seconds&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Mistakes I Made
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Not planning for menu changes
&lt;/h3&gt;

&lt;p&gt;My initial schema had menu items tightly coupled to orders. When the restaurant changed prices or renamed dishes, it broke historical reports. &lt;strong&gt;Fix:&lt;/strong&gt; I added a snapshot of the item at order time — &lt;code&gt;OrderItem&lt;/code&gt; stores its own &lt;code&gt;PriceAtTime&lt;/code&gt; and &lt;code&gt;NameAtTime&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Underestimating receipt formatting
&lt;/h3&gt;

&lt;p&gt;Thermal printers have &lt;strong&gt;42-character line width&lt;/strong&gt; (for 80mm paper). I spent more time formatting receipts than I expected. Arabic/Urdu text support was a whole adventure.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Not building user management from day one
&lt;/h3&gt;

&lt;p&gt;I added multi-user roles (cashier, manager, admin) later, and retrofitting permissions into an existing system is painful. &lt;strong&gt;Always plan roles early.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use .NET 8&lt;/strong&gt; instead of .NET Framework — better performance, cross-platform&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add offline-first architecture&lt;/strong&gt; from the start, not as an afterthought&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build a web dashboard&lt;/strong&gt; alongside the desktop app for owners to check reports remotely&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use message queues&lt;/strong&gt; (RabbitMQ or even a simple one) instead of polling for kitchen display&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;RestoCare+ is running in production at real restaurants. It handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ 3-5 terminals simultaneously&lt;/li&gt;
&lt;li&gt;✅ Thermal receipt printing with proper formatting&lt;/li&gt;
&lt;li&gt;✅ Kitchen display with real-time order updates&lt;/li&gt;
&lt;li&gt;✅ Table management with status tracking&lt;/li&gt;
&lt;li&gt;✅ Daily sales reports and analytics&lt;/li&gt;
&lt;li&gt;✅ Menu management with category organization&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Domain knowledge is your unfair advantage.&lt;/strong&gt; My 4 years in a restaurant made me a better POS developer than someone with 10 years of pure coding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Polling is fine for local networks.&lt;/strong&gt; Don't over-engineer with WebSockets when 3-second polling works perfectly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thermal printing is harder than it looks.&lt;/strong&gt; Budget extra time for this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the boring stuff well.&lt;/strong&gt; Boring features like user roles, audit logs, and error handling are what separate a demo from a product.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;I'm Ahsan Mehmood — a Full-Stack Developer and Co-Founder of &lt;a href="https://xechtech.com" rel="noopener noreferrer"&gt;XechTech&lt;/a&gt;. I share what I learn from building real-world software. Follow for more .NET, Flutter, and AI content.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Connect: &lt;a href="https://linkedin.com/in/iamahsanmehmood" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://github.com/iamahsanmehmood" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://xechtech.com" rel="noopener noreferrer"&gt;XechTech&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>architecture</category>
      <category>database</category>
    </item>
    <item>
      <title>From Restaurant Supervisor to Technical Founder: Shipping 38+ Projects in 5 Years</title>
      <dc:creator>Ahsan Mehmood</dc:creator>
      <pubDate>Mon, 09 Mar 2026 03:25:50 +0000</pubDate>
      <link>https://dev.to/iamahsanmehmood/from-restaurant-supervisor-to-technical-founder-shipping-38-projects-in-5-years-1l4m</link>
      <guid>https://dev.to/iamahsanmehmood/from-restaurant-supervisor-to-technical-founder-shipping-38-projects-in-5-years-1l4m</guid>
      <description>&lt;p&gt;Hey Dev.to 👋 I'm Ahsan Mehmood — a Full-Stack Developer and Co-Founder of XechTech, based in Islamabad, Pakistan.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Journey
&lt;/h2&gt;

&lt;p&gt;In 2017, I was working as a restaurant supervisor. By 2021, I had pivoted into software development full-time. Today, I've shipped &lt;strong&gt;38+ production projects&lt;/strong&gt; across 8+ organizations, serving clients in Pakistan, the US, and Australia.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Build
&lt;/h2&gt;

&lt;p&gt;My core stack is &lt;strong&gt;.NET/C#&lt;/strong&gt;, but I work across the full spectrum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🖥️ &lt;strong&gt;Desktop&lt;/strong&gt;: Multi-terminal POS systems, payroll, accounting tools (C#, .NET, SQL Server)&lt;/li&gt;
&lt;li&gt;📱 &lt;strong&gt;Mobile&lt;/strong&gt;: Cross-platform apps with Flutter (Booktionary, EstiMate Pro, DLP App)&lt;/li&gt;
&lt;li&gt;🌐 &lt;strong&gt;Web&lt;/strong&gt;: React, Laravel, Node.js, TypeScript (pal.gov.pk, xechtech.com)&lt;/li&gt;
&lt;li&gt;🤖 &lt;strong&gt;AI&lt;/strong&gt;: AutoCAD AI Agent, Gemini PC-Commander, LLM-powered workflows&lt;/li&gt;
&lt;li&gt;🏗️ &lt;strong&gt;Engineering&lt;/strong&gt;: 7+ structural tools for RPEQ-certified projects in Australia&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My Company: XechTech
&lt;/h2&gt;

&lt;p&gt;I co-founded &lt;a href="https://xechtech.com" rel="noopener noreferrer"&gt;XechTech&lt;/a&gt; in 2021 with my partner Aaqib Saleem. We build high-end software solutions — from RestoCare+ (a restaurant POS system with thermal printing and kitchen displays) to AI-powered PDF automation tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Projects
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;th&gt;Tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RestoCare+ POS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Multi-terminal restaurant POS&lt;/td&gt;
&lt;td&gt;C#, .NET, SQL Server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;EstiMate Pro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AI-powered PDF automation&lt;/td&gt;
&lt;td&gt;Flutter, Dart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Engineering Suite&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7+ structural analysis tools&lt;/td&gt;
&lt;td&gt;C#, .NET, AutoCAD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PAL Website&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Government website&lt;/td&gt;
&lt;td&gt;PHP, Laravel, Flutter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Booktionary&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dictionary &amp;amp; book app&lt;/td&gt;
&lt;td&gt;Flutter, SQLite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AutoCAD AI Agent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AI-generated floor plans&lt;/td&gt;
&lt;td&gt;Python, Gemini API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What I've Learned
&lt;/h2&gt;

&lt;p&gt;The biggest lesson from shipping 38+ projects: &lt;strong&gt;Solve real problems for real people.&lt;/strong&gt; The fanciest tech stack means nothing if it doesn't solve a pain point.&lt;/p&gt;

&lt;p&gt;I built RestoCare+ because I spent 4 years in a restaurant and knew exactly what was broken. I built engineering tools because construction firms in Australia needed calculations done faster. Every successful project started with understanding the problem deeply.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;I'm currently focused on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI integration into traditional business software&lt;/li&gt;
&lt;li&gt;Growing XechTech's client base internationally&lt;/li&gt;
&lt;li&gt;Sharing what I've learned through writing here on Dev.to&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're a developer in Pakistan building your career, or if you're transitioning into tech from another field — I'd love to connect. Drop a comment or find me on &lt;a href="https://linkedin.com/in/iamahsanmehmood" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; 🤝&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Follow me for posts about .NET, Flutter, AI integration, and building software businesses from Pakistan.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>career</category>
      <category>startup</category>
      <category>beginners</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
