<?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: Novasia</title>
    <description>The latest articles on DEV Community by Novasia (novasiaestate).</description>
    <link>https://dev.to/novasiaestate</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F15380%2F7fb78a5d-e5cd-4cb0-84bc-fea02d4d1416.webp</url>
      <title>DEV Community: Novasia</title>
      <link>https://dev.to/novasiaestate</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/novasiaestate"/>
    <language>en</language>
    <item>
      <title>A rental yield calculator should show the scenario that loses money</title>
      <dc:creator>Dmitrii Kuznetsov</dc:creator>
      <pubDate>Wed, 28 Oct 2026 05:42:29 +0000</pubDate>
      <link>https://dev.to/novasiaestate/a-rental-yield-calculator-should-show-the-scenario-that-loses-money-3gb7</link>
      <guid>https://dev.to/novasiaestate/a-rental-yield-calculator-should-show-the-scenario-that-loses-money-3gb7</guid>
      <description>&lt;p&gt;A positive rental yield is easy to demonstrate with a friendly input. The more useful question for a property-data tool is what happens when occupancy falls while fixed costs and loan payments remain due.&lt;/p&gt;

&lt;p&gt;We added a browser-based &lt;a href="https://podkopov-hub.github.io/southeast-asia-real-estate-analytics/stress-lab.html" rel="noopener noreferrer"&gt;Rental Cashflow Stress Lab&lt;/a&gt; to our existing open-source calculator. It compares explicit scenarios instead of replacing uncertain inputs with country presets.&lt;/p&gt;

&lt;p&gt;This is a project from &lt;a href="https://novasia.estate/" rel="noopener noreferrer"&gt;NovAsia Estate&lt;/a&gt;, where our property research covers Cambodia, Thailand and Bali. All the figures below are synthetic test inputs, not observations about those markets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep capacity rent separate from collected rent
&lt;/h2&gt;

&lt;p&gt;The input we call full-occupancy rent is a capacity assumption:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scenario capacity = full-occupancy annual rent × rent-level multiplier
collected annual rent = scenario capacity × occupancy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, 24,000 at 70% occupancy becomes 16,800 of collected annual rent. A downside scenario uses a lower rent level of 85% and occupancy of 35%, giving 7,140.&lt;/p&gt;

&lt;p&gt;This simple annual fraction does not model seasonality, cancellation patterns or the difference between asking and achieved rent. Making that limitation explicit is preferable to presenting the result as a forecast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixed costs should survive zero occupancy
&lt;/h2&gt;

&lt;p&gt;Our example has 1,200 of annual common-area charges, 500 of other fixed costs and 6,000 of annual debt service. Management and maintenance-reserve assumptions are 20% and 5% of collected rent.&lt;/p&gt;

&lt;p&gt;The core engine calculates operating income and applies the caller's chosen simplified tax base. The stress extension subtracts debt service afterwards. It does not infer tax deductibility of interest, depreciation, a loan balance or an equity-return denominator.&lt;/p&gt;

&lt;p&gt;With an illustrative 10% tax on positive operating profit, the three fixture outcomes are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Occupancy&lt;/th&gt;
&lt;th&gt;Rent level&lt;/th&gt;
&lt;th&gt;Fixed-cost level&lt;/th&gt;
&lt;th&gt;Net annual cash flow after debt&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Base&lt;/td&gt;
&lt;td&gt;70%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;3,810&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vacancy shock&lt;/td&gt;
&lt;td&gt;35%&lt;/td&gt;
&lt;td&gt;85%&lt;/td&gt;
&lt;td&gt;125%&lt;/td&gt;
&lt;td&gt;-3,093&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stronger demand&lt;/td&gt;
&lt;td&gt;90%&lt;/td&gt;
&lt;td&gt;110%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;8,508&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At zero occupancy, those original fixed costs and debt still produce a loss of 7,700. The tool retains that number. A missing cost is an input error; zero must be entered deliberately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Break-even needs the same assumptions as the comparison
&lt;/h2&gt;

&lt;p&gt;Let &lt;code&gt;F&lt;/code&gt; be fixed operating costs, &lt;code&gt;D&lt;/code&gt; annual debt service, &lt;code&gt;k&lt;/code&gt; the fraction of collected rent remaining after management and reserve, and &lt;code&gt;t&lt;/code&gt; the supplied tax rate.&lt;/p&gt;

&lt;p&gt;For a gross-rent tax assumption, the continuous threshold is &lt;code&gt;(F + D) / (k - t)&lt;/code&gt; when the retained margin is positive.&lt;/p&gt;

&lt;p&gt;For tax on positive operating profit, with &lt;code&gt;k &amp;gt; 0&lt;/code&gt; and &lt;code&gt;t &amp;lt; 1&lt;/code&gt;, it is &lt;code&gt;(F + D / (1 - t)) / k&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The example's base case needs about 11,155.56 of collected annual rent, or about 46.48% of its stated capacity. A requirement above 100% occupancy means the entered rent capacity cannot cover the costs. Impossible margins and zero capacity have explicit states rather than a misleading percentage. The exact edge cases, including 100% tax and zero costs, are covered by tests.&lt;/p&gt;

&lt;p&gt;These thresholds are approximate continuous results before cent rounding. Breaking even is not the same as earning a positive return.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the experiment reproducible
&lt;/h2&gt;

&lt;p&gt;The extension uses the same engine as the original calculator:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Stress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./docs/stress.js&lt;/span&gt;&lt;span class="dl"&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;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./examples/stress.json&lt;/span&gt;&lt;span class="dl"&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;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Stress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;annualDebtService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scenarios&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="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;netAfterDebt&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://github.com/podkopov-hub/southeast-asia-real-estate-analytics" rel="noopener noreferrer"&gt;source repository&lt;/a&gt; contains the fixture and 36 passing contracts, including zero rent, retained losses, both tax bases, impossible break-even states, CSV formula protection and browser/Node consistency. JSON exports retain the assumptions and itemized core results. Browser inputs are calculated locally without upload requests.&lt;/p&gt;

&lt;p&gt;For the earlier acquisition stage, our &lt;a href="https://github.com/novasia-estate/payment-milestone-studio" rel="noopener noreferrer"&gt;Payment Milestone Studio&lt;/a&gt; allocates dated installments with exact integer arithmetic. That is a separate problem: a purchase payment schedule does not establish rental income.&lt;/p&gt;

&lt;p&gt;The Stress Lab is a repository and browser extension. The published &lt;code&gt;novasia-sea-yield&lt;/code&gt; npm package remains version 2.0.1; we are not describing unshipped npm exports. Contributions that make assumptions clearer are welcome.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>opensource</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why parsing real estate contracts with LLMs is ten times harder than tutorials claim</title>
      <dc:creator>Dmitrii Kuznetsov</dc:creator>
      <pubDate>Sun, 18 Oct 2026 04:52:15 +0000</pubDate>
      <link>https://dev.to/novasiaestate/why-parsing-real-estate-contracts-with-llms-is-ten-times-harder-than-tutorials-claim-4pak</link>
      <guid>https://dev.to/novasiaestate/why-parsing-real-estate-contracts-with-llms-is-ten-times-harder-than-tutorials-claim-4pak</guid>
      <description>&lt;p&gt;When we started building the data pipeline for &lt;a href="https://novasia.estate/en/" rel="noopener noreferrer"&gt;NovAsia&lt;/a&gt; (a property catalog and market tool for Southeast Asia), the plan looked embarrassingly simple on paper:&lt;/p&gt;

&lt;p&gt;Grab property PDFs and developer contracts (SPAs).&lt;/p&gt;

&lt;p&gt;Run them through a modern vision LLM.&lt;/p&gt;

&lt;p&gt;Dump structured JSON into PostgreSQL.&lt;/p&gt;

&lt;p&gt;Call it a day.&lt;/p&gt;

&lt;p&gt;Two weeks in, the pipeline was completely broken. Here is why standard tutorial advice fell apart in production, and what we had to change to get reliable data out of scanned documents.&lt;/p&gt;

&lt;p&gt;The stuff tutorials don't warn you about&lt;br&gt;
Most LLM document parsing demos use crisp, born-digital PDFs with clean typography. In emerging markets like Cambodia and Thailand, reality looks very different:&lt;/p&gt;

&lt;p&gt;Phone scans: Half the documents are multi-generation scans or photos taken with a phone camera under uneven lighting.&lt;/p&gt;

&lt;p&gt;Dual-language layout: Khmer or Thai text mixed with English, often in adjacent table columns where line baselines don't match.&lt;/p&gt;

&lt;p&gt;Physical stamps: Red rubber stamps, signatures, and notary seals placed directly over critical numbers (payment milestone dates, sinking fund fees, transfer taxes).&lt;/p&gt;

&lt;p&gt;The 40-page prompt trap: If you shove a 35-page PDF into a long-context model and ask for "all fees and payment dates", it will extract 80% correctly and quietly hallucinate the rest with total confidence.&lt;/p&gt;

&lt;p&gt;Three practical changes that fixed it&lt;br&gt;
Instead of trying to find one magic model that does everything, we broke the pipeline down into smaller, boring steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Aggressive image cleanup before text extraction&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Before any model sees the page, we run basic OpenCV passes: deskewing, binarization, and contrast normalization. Removing phone-scan shadows did more for extraction accuracy than upgrading to a larger model tier.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Section-level chunking with rigid schema validation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We stopped asking the model to parse whole documents at once. We chunk by specific section (e.g., "Schedule of Payments", "Maintenance Obligations") and enforce strict Pydantic schemas. If a field fails validation or falls outside expected bounds, the pipeline flags it for manual check rather than guessing.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Treating tables and narrative text as separate problems&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Vision models are great at answering qualitative questions ("under what conditions is the deposit refundable?"), but notoriously sloppy with columns in financial tables. We split table extraction into dedicated passes with explicit row-by-row checks.&lt;/p&gt;

&lt;p&gt;The takeaway&lt;br&gt;
If you're building with LLMs on messy, physical-world paperwork, raw context window size won't save you. Most of the reliability comes from image hygiene and strict schema boundaries.&lt;/p&gt;

&lt;p&gt;Happy to trade notes if anyone else here is wrestling with noisy scans and table extraction.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>webscraping</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
